@unsetsoft/ryunixjs 1.2.5-canary.12 → 1.2.5-canary.13

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.
@@ -432,6 +432,7 @@
432
432
  const el = dom;
433
433
  const domEl = el;
434
434
  const handlerMap = domEl._ryunixHandlers;
435
+ const elRecord = el;
435
436
  Object.keys(prevProps)
436
437
  .filter(isEvent)
437
438
  .filter((key) => isGone(nextProps)(key) || isNew(prevProps, nextProps)(key))
@@ -466,7 +467,7 @@
466
467
  el.removeAttribute(attrName);
467
468
  }
468
469
  else {
469
- el[propKey] = '';
470
+ elRecord[propKey] = '';
470
471
  el.removeAttribute(propKey);
471
472
  }
472
473
  });
@@ -487,8 +488,8 @@
487
488
  }
488
489
  else {
489
490
  if (propKey === 'value' || propKey === 'checked') {
490
- if (el[propKey] !== nextProps[propKey]) {
491
- el[propKey] = nextProps[propKey];
491
+ if (elRecord[propKey] !== nextProps[propKey]) {
492
+ elRecord[propKey] = nextProps[propKey];
492
493
  }
493
494
  }
494
495
  else {
@@ -496,15 +497,15 @@
496
497
  if (isSvgNode) {
497
498
  const attrName = toSvgAttrName(propKey);
498
499
  const svgValidated = checkAttributeUri(attrName, nextProps[propKey]);
499
- el.setAttribute(attrName, svgValidated);
500
+ el.setAttribute(attrName, String(svgValidated));
500
501
  }
501
502
  else {
502
503
  const attrVal = nextProps[propKey];
503
504
  const safeValue = checkAttributeUri(propKey, attrVal);
504
- el[propKey] = safeValue;
505
+ elRecord[propKey] = safeValue;
505
506
  if (typeof attrVal !== 'object' &&
506
507
  typeof attrVal !== 'function') {
507
- el.setAttribute(propKey, safeValue);
508
+ el.setAttribute(propKey, String(safeValue));
508
509
  }
509
510
  }
510
511
  }
@@ -671,9 +672,7 @@
671
672
  }
672
673
  try {
673
674
  const cleanup = hook.effect();
674
- hook.cancel = is.function(cleanup)
675
- ? cleanup
676
- : null;
675
+ hook.cancel = is.function(cleanup) ? cleanup : null;
677
676
  }
678
677
  catch (error) {
679
678
  if (process.env.NODE_ENV !== 'production') {
@@ -705,9 +704,7 @@
705
704
  }
706
705
  try {
707
706
  const cleanup = hook.effect();
708
- hook.cancel = is.function(cleanup)
709
- ? cleanup
710
- : null;
707
+ hook.cancel = is.function(cleanup) ? cleanup : null;
711
708
  }
712
709
  catch (error) {
713
710
  if (process.env.NODE_ENV !== 'production') {
@@ -723,6 +720,8 @@
723
720
  const state = getState();
724
721
  state.deletions.forEach(commitWork);
725
722
  const finishedWork = state.wipRoot;
723
+ if (!finishedWork)
724
+ return;
726
725
  state.currentRoot = finishedWork;
727
726
  if (state.isHydrating || state.hydrationFailed) {
728
727
  if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
@@ -779,6 +778,8 @@
779
778
  return;
780
779
  }
781
780
  const domParent = domParentFiber.dom;
781
+ if (!domParent)
782
+ return;
782
783
  if (fiber.effectTag === EFFECT_TAGS.PLACEMENT) {
783
784
  if (fiber.dom != null) {
784
785
  if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
@@ -792,7 +793,7 @@
792
793
  else if (fiber.effectTag === EFFECT_TAGS.UPDATE) {
793
794
  cancelEffects(fiber);
794
795
  if (fiber.dom != null) {
795
- updateDom(fiber.dom, fiber.alternate.props, fiber.props);
796
+ updateDom(fiber.dom, fiber.alternate?.props, fiber.props);
796
797
  }
797
798
  runLayoutEffects(fiber);
798
799
  runNormalEffects(fiber);
@@ -841,7 +842,7 @@
841
842
  else if (fiber.effectTag === EFFECT_TAGS.UPDATE) {
842
843
  cancelEffects(fiber);
843
844
  if (fiber.dom != null) {
844
- updateDom(fiber.dom, fiber.alternate.props, fiber.props);
845
+ updateDom(fiber.dom, fiber.alternate?.props, fiber.props);
845
846
  }
846
847
  runLayoutEffects(fiber);
847
848
  runNormalEffects(fiber);
@@ -894,6 +895,12 @@
894
895
  let newFiber;
895
896
  const sameType = matchedFiber && element.type === matchedFiber.type;
896
897
  if (sameType && matchedFiber) {
898
+ const matchedType = matchedFiber.type;
899
+ const isErrorBoundary = typeof matchedFiber.type === 'function' &&
900
+ matchedType?.ryunix_type === 'RYUNIX_ERROR_BOUNDARY';
901
+ const preserveBoundaryError = isErrorBoundary &&
902
+ matchedFiber.stateError != null &&
903
+ matchedFiber.child == null;
897
904
  newFiber = {
898
905
  type: matchedFiber.type,
899
906
  props: element.props,
@@ -902,7 +909,7 @@
902
909
  alternate: matchedFiber,
903
910
  effectTag: EFFECT_TAGS.UPDATE,
904
911
  hooks: matchedFiber.hooks,
905
- stateError: matchedFiber.stateError,
912
+ stateError: preserveBoundaryError ? matchedFiber.stateError : undefined,
906
913
  key: element.key,
907
914
  index,
908
915
  };
@@ -970,8 +977,7 @@
970
977
  const maybeTyped = type;
971
978
  if (type &&
972
979
  typeof type === 'function' &&
973
- (maybeTyped?.ryunix_type === 'RYUNIX_HYDRATION_BOUNDARY' ||
974
- maybeTyped?.ryunix_type === 'RYUNIX_SERVER_BOUNDARY')) {
980
+ maybeTyped?.ryunix_type === 'RYUNIX_HYDRATION_BOUNDARY') {
975
981
  return current;
976
982
  }
977
983
  current = current.parent || null;
@@ -1037,11 +1043,15 @@
1037
1043
  const state = getState();
1038
1044
  state.wipFiber = fiber;
1039
1045
  state.hookIndex = 0;
1040
- state.wipFiber.hooks = [];
1046
+ fiber.hooks = [];
1047
+ const componentType = fiber.type;
1048
+ if (state.hydrationRecover &&
1049
+ componentType.ryunix_type === 'RYUNIX_ERROR_BOUNDARY') {
1050
+ fiber.stateError = undefined;
1051
+ }
1041
1052
  if (state.isHydrating) {
1042
1053
  fiber.effectTag = EFFECT_TAGS.HYDRATE;
1043
1054
  }
1044
- const componentType = fiber.type;
1045
1055
  if (componentType._isMemo && fiber.alternate) {
1046
1056
  const { children: _pc, ...prevRest } = fiber.alternate.props || {};
1047
1057
  const { children: _nc, ...nextRest } = fiber.props || {};
@@ -1055,7 +1065,7 @@
1055
1065
  return;
1056
1066
  }
1057
1067
  }
1058
- let children = [
1068
+ const children = [
1059
1069
  componentType(fiber.props),
1060
1070
  ];
1061
1071
  if (componentType._contextId && fiber.props?.value !== undefined) {
@@ -1073,11 +1083,24 @@
1073
1083
  }
1074
1084
  return false;
1075
1085
  };
1086
+ const isUnderServerPreserveBoundary = (fiber) => {
1087
+ let current = fiber?.parent || null;
1088
+ while (current) {
1089
+ if (current._hydratePreserveServer)
1090
+ return true;
1091
+ current = current.parent || null;
1092
+ }
1093
+ return false;
1094
+ };
1095
+ const normalizeChildNodes = (children) => {
1096
+ if (children == null)
1097
+ return [];
1098
+ return Array.isArray(children) ? children : [children];
1099
+ };
1076
1100
  const updateHostComponent = (fiber) => {
1077
1101
  const state = getState();
1078
1102
  if (fiber.type === RYUNIX_TYPES.RYUNIX_CONTEXT) {
1079
- fiber._contextId =
1080
- fiber.props?._contextId;
1103
+ fiber._contextId = fiber.props?._contextId;
1081
1104
  fiber._contextValue = fiber.props?.value;
1082
1105
  }
1083
1106
  const isPassthrough = fiber.type === RYUNIX_TYPES.RYUNIX_FRAGMENT ||
@@ -1086,6 +1109,9 @@
1086
1109
  if (state.isHydrating && isPassthrough) {
1087
1110
  fiber.effectTag = EFFECT_TAGS.HYDRATE;
1088
1111
  }
1112
+ else if (state.isHydrating && isUnderServerPreserveBoundary(fiber)) {
1113
+ return;
1114
+ }
1089
1115
  else if (state.isHydrating && isUnderClientOnlyBoundary(fiber)) {
1090
1116
  if (!fiber.dom) {
1091
1117
  fiber.dom = createDom(fiber);
@@ -1108,6 +1134,13 @@
1108
1134
  domNode.nodeValue = String(fiber.props.nodeValue);
1109
1135
  logHydrationRecoverable('text');
1110
1136
  }
1137
+ if (isElement &&
1138
+ domNode.hasAttribute('data-ryunix-server')) {
1139
+ fiber._hydratePreserveServer = true;
1140
+ state.hydrateCursor = nextValidSibling$1(domNode);
1141
+ reconcileChildren(fiber, []);
1142
+ return;
1143
+ }
1111
1144
  if (isElement &&
1112
1145
  domNode.hasAttribute('data-ryunix-hydrate-boundary')) {
1113
1146
  fiber._hydrateClientOnly = true;
@@ -1116,7 +1149,7 @@
1116
1149
  }
1117
1150
  else {
1118
1151
  const policy = getHydrationPolicy();
1119
- const detail = `Mismatch at ${getTypeLabel(fiber.type)}. Expected ${domNode.nodeType === 1 ? domNode.tagName : 'text'} but got ${String(fiber.type)}.`;
1152
+ const detail = `Mismatch at ${getTypeLabel(fiber.type ?? 'unknown')}. Expected ${domNode.nodeType === 1 ? domNode.tagName : 'text'} but got ${String(fiber.type)}.`;
1120
1153
  const boundaryFiber = findNearestHydrationBoundary(fiber);
1121
1154
  const boundaryDom = (boundaryFiber ? getBoundaryDom(boundaryFiber) : null) ??
1122
1155
  findBoundaryDomFromNode(state.hydrateCursor);
@@ -1148,7 +1181,10 @@
1148
1181
  fiber.dom = createDom(fiber);
1149
1182
  }
1150
1183
  }
1151
- const children = fiber.props?.children || [];
1184
+ if (fiber._hydratePreserveServer) {
1185
+ return;
1186
+ }
1187
+ const children = normalizeChildNodes(fiber.props?.children);
1152
1188
  reconcileChildren(fiber, children);
1153
1189
  };
1154
1190
  const getTypeLabel = (type) => {
@@ -1172,6 +1208,11 @@
1172
1208
  }
1173
1209
  };
1174
1210
 
1211
+ const getRootChild = (children) => {
1212
+ if (children == null)
1213
+ return undefined;
1214
+ return Array.isArray(children) ? children[0] : children;
1215
+ };
1175
1216
  const renderSubtree = (element, container) => {
1176
1217
  clearContainer(container);
1177
1218
  const root = {
@@ -1201,7 +1242,7 @@
1201
1242
  if (policy.recover === 'none')
1202
1243
  return;
1203
1244
  const container = state.containerRoot || state.currentRoot?.dom;
1204
- const element = state.currentRoot?.props?.children?.[0];
1245
+ const element = getRootChild(state.currentRoot?.props?.children);
1205
1246
  if (!container || element == null)
1206
1247
  return;
1207
1248
  state.hydrationRecover = true;
@@ -1211,8 +1252,10 @@
1211
1252
  renderSubtree(element, container);
1212
1253
  };
1213
1254
  const runHydrationRecovery = () => {
1255
+ const state = getState();
1214
1256
  recoverScopedHydrationFailures();
1215
1257
  recoverHydrationFailureIfNeeded();
1258
+ state.hydrationRecover = false;
1216
1259
  };
1217
1260
 
1218
1261
  let workQueue = [];
@@ -1232,32 +1275,36 @@
1232
1275
  if (process.env.NODE_ENV !== 'production') {
1233
1276
  console.error('[Ryunix ErrorBoundary] Caught error during render:', error);
1234
1277
  try {
1235
- const src = fiber.props && fiber.props.__source;
1278
+ const fiberProps = fiber.props;
1279
+ const src = fiberProps?.__source;
1236
1280
  if (src && error && typeof error === 'object') {
1281
+ ;
1237
1282
  error.__ryunix_source = src;
1238
1283
  }
1239
1284
  let targetFiber = fiber;
1240
1285
  while (!error.__ryunix_source && targetFiber) {
1241
- if (targetFiber.props && targetFiber.props.__source) {
1242
- error.__ryunix_source = targetFiber.props.__source;
1286
+ const targetProps = targetFiber.props;
1287
+ if (targetProps?.__source) {
1288
+ ;
1289
+ error.__ryunix_source = targetProps.__source;
1243
1290
  }
1244
1291
  targetFiber = targetFiber.parent;
1245
1292
  }
1246
1293
  }
1247
- catch (e) { }
1294
+ catch (_e) { }
1248
1295
  }
1249
1296
  let boundaryFiber = fiber.parent;
1250
1297
  let foundBoundary = false;
1251
1298
  while (boundaryFiber) {
1252
1299
  if (boundaryFiber.type &&
1253
- boundaryFiber.type
1254
- .ryunix_type === 'RYUNIX_ERROR_BOUNDARY') {
1300
+ boundaryFiber.type.ryunix_type ===
1301
+ 'RYUNIX_ERROR_BOUNDARY') {
1255
1302
  foundBoundary = true;
1256
1303
  break;
1257
1304
  }
1258
1305
  boundaryFiber = boundaryFiber.parent;
1259
1306
  }
1260
- if (foundBoundary) {
1307
+ if (foundBoundary && boundaryFiber) {
1261
1308
  if (process.env.NODE_ENV !== 'production') {
1262
1309
  console.warn('[Ryunix ErrorBoundary] Recovering tree at nearest boundary.');
1263
1310
  }
@@ -1284,6 +1331,7 @@
1284
1331
  }
1285
1332
  nextFiber = nextFiber.parent;
1286
1333
  }
1334
+ return null;
1287
1335
  }
1288
1336
  const workLoop = (deadline) => {
1289
1337
  const state = getState();
@@ -1291,6 +1339,8 @@
1291
1339
  while ((state.nextUnitOfWork || workQueue.length > 0) && !shouldYield) {
1292
1340
  if (!state.nextUnitOfWork && workQueue.length > 0) {
1293
1341
  const nextRoot = workQueue.shift();
1342
+ if (!nextRoot)
1343
+ continue;
1294
1344
  state.wipRoot = nextRoot;
1295
1345
  state.nextUnitOfWork = nextRoot;
1296
1346
  state.deletions = [];
@@ -1351,9 +1401,7 @@
1351
1401
  const root = {
1352
1402
  dom: container,
1353
1403
  props: {
1354
- children: [
1355
- element,
1356
- ],
1404
+ children: [element],
1357
1405
  },
1358
1406
  alternate: state.currentRoot,
1359
1407
  isHydrating: false,
@@ -1366,7 +1414,7 @@
1366
1414
  const nextValidSibling = (node) => {
1367
1415
  let next = node;
1368
1416
  while (next &&
1369
- ((next.nodeType === 3 && !next.nodeValue.trim()) ||
1417
+ ((next.nodeType === 3 && !next.nodeValue?.trim()) ||
1370
1418
  next.nodeType === 8 ||
1371
1419
  (next.nodeType === 1 &&
1372
1420
  next.hasAttribute('data-ryunix-ssr')))) {
@@ -1380,9 +1428,7 @@
1380
1428
  const root = {
1381
1429
  dom: container,
1382
1430
  props: {
1383
- children: [
1384
- element,
1385
- ],
1431
+ children: [element],
1386
1432
  },
1387
1433
  alternate: state.currentRoot,
1388
1434
  isHydrating: true,
@@ -1391,7 +1437,7 @@
1391
1437
  scheduleWork(root);
1392
1438
  return root;
1393
1439
  };
1394
- const init = (MainElement, root = '__ryunix', components = {}) => {
1440
+ const init = (MainElement, root = '__ryunix', _components = {}) => {
1395
1441
  const state = getState();
1396
1442
  const container = document.getElementById(root);
1397
1443
  state.containerRoot = container;
@@ -1488,6 +1534,82 @@
1488
1534
  }
1489
1535
  };
1490
1536
 
1537
+ const INTERNAL_META_KEYS = new Set([
1538
+ 'title',
1539
+ 'pageTitle',
1540
+ 'canonical',
1541
+ 'titleTemplate',
1542
+ 'titleDefault',
1543
+ 'lastmod',
1544
+ 'changefreq',
1545
+ 'priority',
1546
+ 'custom',
1547
+ 'icon',
1548
+ 'appleTouchIcon',
1549
+ ]);
1550
+ const isTitleConfig = (value) => is.object(value) &&
1551
+ value !== null &&
1552
+ ('default' in value || 'template' in value);
1553
+ const pickString = (value) => typeof value === 'string' && value.trim() ? value.trim() : undefined;
1554
+ function resolvePageMetadata(meta = {}, options = {}) {
1555
+ const template = pickString(options.title?.template) ||
1556
+ pickString(meta.titleTemplate) ||
1557
+ (isTitleConfig(meta.title) ? pickString(meta.title.template) : undefined);
1558
+ const defaultTitle = pickString(options.title?.prefix) ||
1559
+ pickString(meta.titleDefault) ||
1560
+ (isTitleConfig(meta.title) ? pickString(meta.title.default) : undefined) ||
1561
+ 'Ryunix App';
1562
+ const pageTitle = pickString(meta.pageTitle) ||
1563
+ (typeof meta.title === 'string' ? pickString(meta.title) : undefined);
1564
+ let title = defaultTitle;
1565
+ if (pageTitle) {
1566
+ title =
1567
+ template && template.includes('%s')
1568
+ ? template.replace('%s', pageTitle)
1569
+ : pageTitle;
1570
+ }
1571
+ const tags = {};
1572
+ for (const [key, value] of Object.entries(meta)) {
1573
+ if (INTERNAL_META_KEYS.has(key))
1574
+ continue;
1575
+ if (key === 'title' && isTitleConfig(value))
1576
+ continue;
1577
+ if (Array.isArray(value)) {
1578
+ const items = value
1579
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
1580
+ .filter(Boolean);
1581
+ if (items.length > 0)
1582
+ tags[key] = items;
1583
+ continue;
1584
+ }
1585
+ if (typeof value === 'string' && value.trim()) {
1586
+ tags[key] = value.trim();
1587
+ }
1588
+ }
1589
+ if (pickString(meta.canonical)) {
1590
+ tags.canonical = meta.canonical;
1591
+ }
1592
+ if (pickString(meta.icon)) {
1593
+ tags.icon = meta.icon;
1594
+ }
1595
+ if (pickString(meta.appleTouchIcon)) {
1596
+ tags.appleTouchIcon = meta.appleTouchIcon;
1597
+ }
1598
+ return { title, tags };
1599
+ }
1600
+ function mergeRouteMetadata(base = {}, next = {}) {
1601
+ const merged = { ...base, ...next };
1602
+ if (typeof next.title === 'string' && isTitleConfig(base.title)) {
1603
+ if (!pickString(merged.titleTemplate) && pickString(base.title.template)) {
1604
+ merged.titleTemplate = base.title.template;
1605
+ }
1606
+ if (!pickString(merged.titleDefault) && pickString(base.title.default)) {
1607
+ merged.titleDefault = base.title.default;
1608
+ }
1609
+ }
1610
+ return merged;
1611
+ }
1612
+
1491
1613
  const haveDepsChanged = (oldDeps, newDeps) => {
1492
1614
  if (!oldDeps || !newDeps)
1493
1615
  return true;
@@ -1527,6 +1649,8 @@
1527
1649
  validateHookContext();
1528
1650
  const { hookIndex } = state;
1529
1651
  const wipFiber = state.wipFiber;
1652
+ if (!wipFiber.hooks)
1653
+ wipFiber.hooks = [];
1530
1654
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1531
1655
  const hook = {
1532
1656
  hookID: hookIndex,
@@ -1566,8 +1690,7 @@
1566
1690
  };
1567
1691
  queueUpdate(() => scheduleWork$1(newRoot, priority));
1568
1692
  };
1569
- wipFiber.hooks[hookIndex] =
1570
- hook;
1693
+ wipFiber.hooks[hookIndex] = hook;
1571
1694
  state.hookIndex++;
1572
1695
  return [hook.state, dispatch];
1573
1696
  };
@@ -1588,6 +1711,8 @@
1588
1711
  }
1589
1712
  const { hookIndex } = state;
1590
1713
  const wipFiber = state.wipFiber;
1714
+ if (!wipFiber.hooks)
1715
+ wipFiber.hooks = [];
1591
1716
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1592
1717
  const hasChanged = haveDepsChanged(oldHook?.deps, deps);
1593
1718
  const hook = {
@@ -1597,8 +1722,7 @@
1597
1722
  effect: hasChanged ? callback : null,
1598
1723
  cancel: oldHook?.cancel,
1599
1724
  };
1600
- wipFiber.hooks[hookIndex] =
1601
- hook;
1725
+ wipFiber.hooks[hookIndex] = hook;
1602
1726
  state.hookIndex++;
1603
1727
  };
1604
1728
  const useRef = (initialValue) => {
@@ -1612,6 +1736,8 @@
1612
1736
  validateHookContext();
1613
1737
  const { hookIndex } = state;
1614
1738
  const wipFiber = state.wipFiber;
1739
+ if (!wipFiber.hooks)
1740
+ wipFiber.hooks = [];
1615
1741
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1616
1742
  const hook = {
1617
1743
  hookID: hookIndex,
@@ -1620,8 +1746,7 @@
1620
1746
  ? oldHook.value
1621
1747
  : { current: initialValue },
1622
1748
  };
1623
- wipFiber.hooks[hookIndex] =
1624
- hook;
1749
+ wipFiber.hooks[hookIndex] = hook;
1625
1750
  state.hookIndex++;
1626
1751
  return hook.value;
1627
1752
  };
@@ -1642,6 +1767,8 @@
1642
1767
  }
1643
1768
  const { hookIndex } = state;
1644
1769
  const wipFiber = state.wipFiber;
1770
+ if (!wipFiber.hooks)
1771
+ wipFiber.hooks = [];
1645
1772
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1646
1773
  let value;
1647
1774
  if (oldHook && !haveDepsChanged(oldHook.deps, deps)) {
@@ -1664,8 +1791,7 @@
1664
1791
  value,
1665
1792
  deps,
1666
1793
  };
1667
- wipFiber.hooks[hookIndex] =
1668
- hook;
1794
+ wipFiber.hooks[hookIndex] = hook;
1669
1795
  state.hookIndex++;
1670
1796
  return value;
1671
1797
  };
@@ -1676,7 +1802,7 @@
1676
1802
  return useMemo(() => callback, deps);
1677
1803
  };
1678
1804
  const createContext = (contextId = RYUNIX_TYPES.RYUNIX_CONTEXT, defaultValue = {}) => {
1679
- const Provider = ({ value, children }) => {
1805
+ const Provider = ({ value, children, }) => {
1680
1806
  return createElement(RYUNIX_TYPES.RYUNIX_CONTEXT, { value, children, _contextId: contextId }, ...flattenArray([children]));
1681
1807
  };
1682
1808
  Provider._contextId = contextId;
@@ -1731,7 +1857,7 @@
1731
1857
  const useMetadata = (tags = {}, options = {}) => {
1732
1858
  const state = getState();
1733
1859
  if (state.isServerRendering) {
1734
- state.ssrMetadata = { ...state.ssrMetadata, ...tags };
1860
+ state.ssrMetadata = mergeRouteMetadata((state.ssrMetadata || {}), tags);
1735
1861
  return;
1736
1862
  }
1737
1863
  useEffect(() => {
@@ -1762,6 +1888,8 @@
1762
1888
  Object.entries(tags).forEach(([key, value]) => {
1763
1889
  if (['title', 'pageTitle', 'canonical'].includes(key))
1764
1890
  return;
1891
+ if (value == null)
1892
+ return;
1765
1893
  const isProperty = key.startsWith('og:') || key.startsWith('twitter:');
1766
1894
  const selector = `meta[${isProperty ? 'property' : 'name'}='${key}']`;
1767
1895
  let meta = document.head.querySelector(selector);
@@ -1785,7 +1913,7 @@
1785
1913
  const pathname = path.split('?')[0].split('#')[0];
1786
1914
  const notFoundRoute = routes.find((route) => route.NotFound);
1787
1915
  const notFound = notFoundRoute
1788
- ? { route: { component: notFoundRoute.NotFound }, params: {} }
1916
+ ? { route: { component: notFoundRoute.NotFound ?? null }, params: {} }
1789
1917
  : { route: { component: null }, params: {} };
1790
1918
  for (const route of routes) {
1791
1919
  if (route.subRoutes) {
@@ -1821,7 +1949,7 @@
1821
1949
  }
1822
1950
  return '/';
1823
1951
  };
1824
- const RouterProvider = ({ routes, children }) => {
1952
+ const RouterProvider = ({ routes, children, }) => {
1825
1953
  if (typeof window === 'undefined') {
1826
1954
  const location = getSsrPathname();
1827
1955
  const currentRouteData = findRoute(routes, location);
@@ -1855,7 +1983,7 @@
1855
1983
  const currentRouteData = findRoute(routes, location);
1856
1984
  const query = useQuery();
1857
1985
  const contextValue = {
1858
- location,
1986
+ location: location,
1859
1987
  params: currentRouteData.params || {},
1860
1988
  query,
1861
1989
  navigate,
@@ -1877,10 +2005,13 @@
1877
2005
  const el = document.getElementById(id);
1878
2006
  if (el)
1879
2007
  el.scrollIntoView({ block: 'start', behavior: 'smooth' });
2008
+ return;
2009
+ }
2010
+ if (typeof window !== 'undefined') {
2011
+ window.scrollTo(0, 0);
1880
2012
  }
1881
- }, [hash]);
2013
+ }, [location, hash]);
1882
2014
  return createElement(route.component, {
1883
- key: location,
1884
2015
  params,
1885
2016
  query,
1886
2017
  hash,
@@ -1911,14 +2042,14 @@
1911
2042
  href: to,
1912
2043
  onClick: handleClick,
1913
2044
  onMouseEnter: handleMouseEnter,
1914
- className: props.className || props['ryunix-class'],
2045
+ className: (props.className || props['ryunix-class']),
1915
2046
  ...cleanedProps,
1916
2047
  }, props.children);
1917
2048
  };
1918
2049
  const NavLink = ({ to, exact = false, ...props }) => {
1919
2050
  const { location, navigate } = useRouter();
1920
2051
  const isActive = exact ? location === to : location.startsWith(to);
1921
- const resolveClass = (cls) => typeof cls === 'function' ? cls({ isActive }) : cls || '';
2052
+ const resolveClass = (cls) => (typeof cls === 'function' ? cls({ isActive }) : cls || '');
1922
2053
  const handleClick = (e) => {
1923
2054
  if (e.button !== 0 || e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
1924
2055
  return;
@@ -1927,7 +2058,7 @@
1927
2058
  navigate(to);
1928
2059
  };
1929
2060
  const classAttrName = props['ryunix-class'] ? 'ryunix-class' : 'className';
1930
- const classAttrValue = resolveClass(props['ryunix-class'] || props.className);
2061
+ const classAttrValue = resolveClass((props['ryunix-class'] || props.className));
1931
2062
  const { ['ryunix-class']: _omitRyunix, className: _omitClassName, ...cleanedProps } = props;
1932
2063
  return createElement('a', {
1933
2064
  href: to,
@@ -1979,7 +2110,7 @@
1979
2110
  const item = window.localStorage.getItem(key);
1980
2111
  return item ? JSON.parse(item) : initialState;
1981
2112
  }
1982
- catch (error) {
2113
+ catch (_error) {
1983
2114
  return initialState;
1984
2115
  }
1985
2116
  });
@@ -2018,6 +2149,8 @@
2018
2149
  }
2019
2150
  const { hookIndex } = state;
2020
2151
  const wipFiber = state.wipFiber;
2152
+ if (!wipFiber.hooks)
2153
+ wipFiber.hooks = [];
2021
2154
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
2022
2155
  const hasChanged = haveDepsChanged(oldHook?.deps, deps);
2023
2156
  const hook = {
@@ -2028,8 +2161,7 @@
2028
2161
  cancel: oldHook?.cancel,
2029
2162
  isLayout: true,
2030
2163
  };
2031
- wipFiber.hooks[hookIndex] =
2032
- hook;
2164
+ wipFiber.hooks[hookIndex] = hook;
2033
2165
  state.hookIndex++;
2034
2166
  };
2035
2167
  let idCounter = 0;
@@ -2044,16 +2176,15 @@
2044
2176
  validateHookContext();
2045
2177
  const { hookIndex } = state;
2046
2178
  const wipFiber = state.wipFiber;
2179
+ if (!wipFiber.hooks)
2180
+ wipFiber.hooks = [];
2047
2181
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
2048
2182
  const hook = {
2049
2183
  hookID: hookIndex,
2050
2184
  type: RYUNIX_TYPES.RYUNIX_REF,
2051
- value: oldHook
2052
- ? oldHook.value
2053
- : `:r${idCounter++}:`,
2185
+ value: oldHook ? oldHook.value : `:r${idCounter++}:`,
2054
2186
  };
2055
- wipFiber.hooks[hookIndex] =
2056
- hook;
2187
+ wipFiber.hooks[hookIndex] = hook;
2057
2188
  state.hookIndex++;
2058
2189
  return hook.value;
2059
2190
  };
@@ -2088,6 +2219,7 @@
2088
2219
  return throttledValue;
2089
2220
  };
2090
2221
 
2222
+ const ASYNC_RENDER_ERROR = 'Async components require renderToStringAsync or renderToReadableStream, not renderToString';
2091
2223
  const escapeHtml = (unsafe) => {
2092
2224
  if (typeof unsafe !== 'string')
2093
2225
  return String(unsafe);
@@ -2122,6 +2254,83 @@
2122
2254
  'track',
2123
2255
  'wbr',
2124
2256
  ]);
2257
+ const normalizeChildren = (children) => {
2258
+ if (children == null)
2259
+ return [];
2260
+ return Array.isArray(children) ? children : [children];
2261
+ };
2262
+ const assertSyncRenderResult = (rendered) => {
2263
+ if (rendered != null &&
2264
+ typeof rendered === 'object' &&
2265
+ 'then' in rendered &&
2266
+ typeof rendered.then === 'function') {
2267
+ throw new Error(ASYNC_RENDER_ERROR);
2268
+ }
2269
+ return rendered;
2270
+ };
2271
+ const buildHostProps = (props, renderChild) => {
2272
+ let attributes = '';
2273
+ let htmlChildren = '';
2274
+ let innerHTML = null;
2275
+ Object.entries(props).forEach(([key, value]) => {
2276
+ if (key === 'children') {
2277
+ if (Array.isArray(value)) {
2278
+ htmlChildren = value
2279
+ .map((child) => renderChild(child))
2280
+ .join('');
2281
+ }
2282
+ else {
2283
+ htmlChildren = renderChild(value);
2284
+ }
2285
+ }
2286
+ else if (key === 'dangerouslySetInnerHTML') {
2287
+ const inner = value;
2288
+ if (inner?.__html) {
2289
+ innerHTML = inner.__html;
2290
+ }
2291
+ }
2292
+ else if (key === STRINGS.STYLE || key === OLD_STRINGS.STYLE) {
2293
+ const styleString = renderStyle(value);
2294
+ if (styleString) {
2295
+ attributes += ` style="${escapeHtml(styleString)}"`;
2296
+ }
2297
+ }
2298
+ else if (key === STRINGS.CLASS_NAME || key === OLD_STRINGS.CLASS_NAME) {
2299
+ if (value) {
2300
+ attributes += ` class="${escapeHtml(value)}"`;
2301
+ }
2302
+ }
2303
+ else if (!key.startsWith('on') &&
2304
+ key !== 'key' &&
2305
+ key !== 'ref' &&
2306
+ key !== '__source' &&
2307
+ key !== '__self') {
2308
+ if (typeof value === 'boolean') {
2309
+ if (value)
2310
+ attributes += ` ${key}=""`;
2311
+ }
2312
+ else if (value != null) {
2313
+ const attrName = toSvgAttrName(key);
2314
+ const validatedValue = validateUri(attrName, value);
2315
+ attributes += ` ${attrName}="${escapeHtml(validatedValue)}"`;
2316
+ }
2317
+ }
2318
+ });
2319
+ return { attributes, innerHTML, htmlChildren };
2320
+ };
2321
+ const formatHostOpenTag = (hostTag, attributes) => {
2322
+ if (VOID_ELEMENTS.has(hostTag)) {
2323
+ return `<${hostTag}${attributes} />`;
2324
+ }
2325
+ return `<${hostTag}${attributes}>`;
2326
+ };
2327
+ const beginSsrRender = () => {
2328
+ const state = getState();
2329
+ state.isServerRendering = true;
2330
+ state.ssrMetadata = {};
2331
+ state.ssrContexts = {};
2332
+ resetIdCounter();
2333
+ };
2125
2334
  const renderToStringImpl = (element) => {
2126
2335
  if (element == null || typeof element === 'boolean') {
2127
2336
  return '';
@@ -2134,20 +2343,18 @@
2134
2343
  }
2135
2344
  const vnode = element;
2136
2345
  if (vnode.type === RYUNIX_TYPES.TEXT_ELEMENT) {
2137
- return escapeHtml(vnode.props
2138
- .nodeValue);
2346
+ return escapeHtml(vnode.props.nodeValue);
2139
2347
  }
2140
2348
  if (vnode.type === RYUNIX_TYPES.RYUNIX_FRAGMENT) {
2141
- const children = vnode.props?.children || [];
2349
+ const children = normalizeChildren(vnode.props?.children);
2142
2350
  return children.map((child) => renderToStringImpl(child)).join('');
2143
2351
  }
2144
2352
  if (vnode.type === RYUNIX_TYPES.RYUNIX_CONTEXT) {
2145
2353
  const state = getState();
2146
2354
  state.ssrContexts = state.ssrContexts || {};
2147
- const ctxProps = vnode.props ||
2148
- {};
2355
+ const ctxProps = (vnode.props || {});
2149
2356
  const ctxId = ctxProps._contextId;
2150
- const prevCtx = state.ssrContexts[ctxId];
2357
+ const prevCtx = ctxId ? state.ssrContexts[ctxId] : undefined;
2151
2358
  if (ctxId) {
2152
2359
  state.ssrContexts[ctxId] = ctxProps.value;
2153
2360
  }
@@ -2167,61 +2374,17 @@
2167
2374
  if (typeof vnode.type === 'function') {
2168
2375
  const type = vnode.type;
2169
2376
  const props = vnode.props || {};
2170
- const renderedElement = type(props);
2377
+ const renderedElement = assertSyncRenderResult(type(props));
2171
2378
  return renderToStringImpl(renderedElement);
2172
2379
  }
2173
2380
  const type = String(vnode.type);
2174
2381
  const props = vnode.props || {};
2175
- let attributes = '';
2176
- let htmlChildren = '';
2177
- let innerHTML = null;
2178
- Object.entries(props).forEach(([key, value]) => {
2179
- if (key === 'children') {
2180
- if (Array.isArray(value)) {
2181
- htmlChildren = value.map((child) => renderToStringImpl(child)).join('');
2182
- }
2183
- else {
2184
- htmlChildren = renderToStringImpl(value);
2185
- }
2186
- }
2187
- else if (key === 'dangerouslySetInnerHTML') {
2188
- const inner = value;
2189
- if (inner?.__html) {
2190
- innerHTML = inner.__html;
2191
- }
2192
- }
2193
- else if (key === STRINGS.STYLE || key === OLD_STRINGS.STYLE) {
2194
- const styleString = renderStyle(value);
2195
- if (styleString) {
2196
- attributes += ` style="${escapeHtml(styleString)}"`;
2197
- }
2198
- }
2199
- else if (key === STRINGS.CLASS_NAME || key === OLD_STRINGS.CLASS_NAME) {
2200
- if (value) {
2201
- attributes += ` class="${escapeHtml(value)}"`;
2202
- }
2203
- }
2204
- else if (!key.startsWith('on') &&
2205
- key !== 'key' &&
2206
- key !== 'ref' &&
2207
- key !== '__source' &&
2208
- key !== '__self') {
2209
- if (typeof value === 'boolean') {
2210
- if (value)
2211
- attributes += ` ${key}=""`;
2212
- }
2213
- else if (value != null) {
2214
- let attrName = toSvgAttrName(key);
2215
- let validatedValue = validateUri(attrName, value);
2216
- attributes += ` ${attrName}="${escapeHtml(validatedValue)}"`;
2217
- }
2218
- }
2219
- });
2382
+ const { attributes, innerHTML, htmlChildren } = buildHostProps(props, renderToStringImpl);
2220
2383
  if (VOID_ELEMENTS.has(type)) {
2221
- return `<${type}${attributes} />`;
2384
+ return formatHostOpenTag(type, attributes);
2222
2385
  }
2223
2386
  const finalContent = innerHTML !== null ? innerHTML : htmlChildren;
2224
- return `<${type}${attributes}>${finalContent}</${type}>`;
2387
+ return `${formatHostOpenTag(type, attributes)}${finalContent}</${type}>`;
2225
2388
  };
2226
2389
  const RC_SCRIPT = `
2227
2390
  function $RC(id, templateId) {
@@ -2236,13 +2399,11 @@ function $RC(id, templateId) {
2236
2399
  .replace(/\s+/g, ' ')
2237
2400
  .trim();
2238
2401
  const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
2239
- if (element == null || typeof element === 'boolean') {
2240
- return;
2241
- }
2242
2402
  if (element instanceof Promise) {
2243
2403
  element = await element;
2244
- if (element == null || typeof element === 'boolean')
2245
- return;
2404
+ }
2405
+ if (element == null || typeof element === 'boolean') {
2406
+ return;
2246
2407
  }
2247
2408
  if (typeof element === 'string' || typeof element === 'number') {
2248
2409
  push(escapeHtml(element));
@@ -2256,12 +2417,11 @@ function $RC(id, templateId) {
2256
2417
  }
2257
2418
  const vnode = element;
2258
2419
  if (vnode.type === RYUNIX_TYPES.TEXT_ELEMENT) {
2259
- push(escapeHtml(vnode
2260
- .props.nodeValue));
2420
+ push(escapeHtml(vnode.props.nodeValue));
2261
2421
  return;
2262
2422
  }
2263
2423
  if (vnode.type === RYUNIX_TYPES.RYUNIX_FRAGMENT) {
2264
- const children = vnode.props?.children || [];
2424
+ const children = normalizeChildren(vnode.props?.children);
2265
2425
  for (const child of children) {
2266
2426
  await renderToStreamImpl(child, push, suspenseTasks);
2267
2427
  }
@@ -2270,10 +2430,9 @@ function $RC(id, templateId) {
2270
2430
  if (vnode.type === RYUNIX_TYPES.RYUNIX_CONTEXT) {
2271
2431
  const state = getState();
2272
2432
  state.ssrContexts = state.ssrContexts || {};
2273
- const ctxProps = vnode.props ||
2274
- {};
2433
+ const ctxProps = (vnode.props || {});
2275
2434
  const ctxId = ctxProps._contextId;
2276
- const prevCtx = state.ssrContexts[ctxId];
2435
+ const prevCtx = ctxId ? state.ssrContexts[ctxId] : undefined;
2277
2436
  if (ctxId) {
2278
2437
  state.ssrContexts[ctxId] = ctxProps.value;
2279
2438
  }
@@ -2295,11 +2454,9 @@ function $RC(id, templateId) {
2295
2454
  const isSuspenseBoundary = vnode.type === RYUNIX_TYPES.RYUNIX_SUSPENSE ||
2296
2455
  (typeof suspenseType === 'object' &&
2297
2456
  suspenseType != null &&
2298
- suspenseType.type ===
2299
- RYUNIX_TYPES.RYUNIX_SUSPENSE);
2457
+ suspenseType.type === RYUNIX_TYPES.RYUNIX_SUSPENSE);
2300
2458
  if (isSuspenseBoundary) {
2301
- const suspenseProps = vnode.props ||
2302
- {};
2459
+ const suspenseProps = (vnode.props || {});
2303
2460
  const { fallback, children } = suspenseProps;
2304
2461
  const id = `s-${Math.random().toString(36).slice(2, 9)}`;
2305
2462
  push(`<!--$?--><template id="B:${id}"></template><div id="S:${id}">`);
@@ -2321,14 +2478,14 @@ function $RC(id, templateId) {
2321
2478
  finally {
2322
2479
  state.isSuspenseBackground = wasBackground;
2323
2480
  }
2324
- })();
2481
+ });
2325
2482
  suspenseTasks.push(task);
2326
2483
  await renderToStreamImpl(fallback, push, suspenseTasks);
2327
2484
  push(`</div><!--$/-->`);
2328
2485
  return;
2329
2486
  }
2330
- let type = vnode.type;
2331
- let props = vnode.props || {};
2487
+ const type = vnode.type;
2488
+ const props = vnode.props || {};
2332
2489
  if (typeof type === 'function') {
2333
2490
  if (process.env.RYUNIX_DEBUG) {
2334
2491
  console.log('[SSR Debug] Rendering function:', type.name || 'anonymous');
@@ -2338,49 +2495,13 @@ function $RC(id, templateId) {
2338
2495
  return;
2339
2496
  }
2340
2497
  const hostTag = String(type);
2341
- let attributes = '';
2342
- let innerHTML = null;
2343
- let children = props.children || [];
2344
- Object.entries(props).forEach(([key, value]) => {
2345
- if (key === 'children') ;
2346
- else if (key === 'dangerouslySetInnerHTML') {
2347
- const inner = value;
2348
- if (inner?.__html) {
2349
- innerHTML = inner.__html;
2350
- }
2351
- }
2352
- else if (key === STRINGS.STYLE || key === OLD_STRINGS.STYLE) {
2353
- const styleString = renderStyle(value);
2354
- if (styleString) {
2355
- attributes += ` style="${escapeHtml(styleString)}"`;
2356
- }
2357
- }
2358
- else if (key === STRINGS.CLASS_NAME || key === OLD_STRINGS.CLASS_NAME) {
2359
- if (value) {
2360
- attributes += ` class="${escapeHtml(value)}"`;
2361
- }
2362
- }
2363
- else if (!key.startsWith('on') &&
2364
- key !== 'key' &&
2365
- key !== 'ref' &&
2366
- key !== '__source' &&
2367
- key !== '__self') {
2368
- if (typeof value === 'boolean') {
2369
- if (value)
2370
- attributes += ` ${key}=""`;
2371
- }
2372
- else if (value != null) {
2373
- const attrName = toSvgAttrName(key);
2374
- const validatedValue = validateUri(attrName, value);
2375
- attributes += ` ${attrName}="${escapeHtml(validatedValue)}"`;
2376
- }
2377
- }
2378
- });
2379
- push(`<${hostTag}${attributes}>`);
2498
+ const children = props.children || [];
2499
+ const { attributes, innerHTML } = buildHostProps(props, () => '');
2500
+ push(formatHostOpenTag(hostTag, attributes));
2380
2501
  if (innerHTML !== null) {
2381
2502
  push(innerHTML);
2382
2503
  }
2383
- else {
2504
+ else if (!VOID_ELEMENTS.has(hostTag)) {
2384
2505
  if (Array.isArray(children)) {
2385
2506
  for (const child of children) {
2386
2507
  await renderToStreamImpl(child, push, suspenseTasks);
@@ -2389,20 +2510,30 @@ function $RC(id, templateId) {
2389
2510
  else {
2390
2511
  await renderToStreamImpl(children, push, suspenseTasks);
2391
2512
  }
2392
- }
2393
- if (!VOID_ELEMENTS.has(hostTag)) {
2394
2513
  push(`</${hostTag}>`);
2395
2514
  }
2396
2515
  };
2516
+ const handleSuspenseTaskResult = (res, push, nonceAttr) => {
2517
+ if (res.success) {
2518
+ push(`<template id="P:${res.id}" data-ryunix-ssr>${res.content}</template>`);
2519
+ push(`<script${nonceAttr} data-ryunix-ssr>$RC("S:${res.id}", "P:${res.id}")</script>`);
2520
+ return;
2521
+ }
2522
+ const message = res.error instanceof Error
2523
+ ? res.error.message
2524
+ : String(res.error ?? 'Unknown error');
2525
+ if (process.env.NODE_ENV !== 'production') {
2526
+ console.error('[Ryunix SSR] Suspense boundary failed:', res.error);
2527
+ }
2528
+ push(`<!-- Ryunix Suspense error: ${escapeHtml(message)} -->`);
2529
+ };
2397
2530
  const renderToReadableStream = (element, options = {}) => {
2398
2531
  const state = getState();
2399
2532
  const encoder = new TextEncoder();
2400
- resetIdCounter();
2401
2533
  return new ReadableStream({
2402
2534
  async start(controller) {
2403
2535
  const wasServerRendering = state.isServerRendering;
2404
- state.isServerRendering = true;
2405
- state.ssrMetadata = {};
2536
+ beginSsrRender();
2406
2537
  const push = (text) => controller.enqueue(encoder.encode(text));
2407
2538
  const suspenseTasks = [];
2408
2539
  try {
@@ -2411,11 +2542,10 @@ function $RC(id, templateId) {
2411
2542
  await renderToStreamImpl(element, push, suspenseTasks);
2412
2543
  while (suspenseTasks.length > 0) {
2413
2544
  const task = suspenseTasks.shift();
2414
- const res = await task;
2415
- if (res.success) {
2416
- push(`<template id="P:${res.id}" data-ryunix-ssr>${res.content}</template>`);
2417
- push(`<script${nonceAttr} data-ryunix-ssr>$RC("S:${res.id}", "P:${res.id}")</script>`);
2418
- }
2545
+ if (!task)
2546
+ continue;
2547
+ const res = await task();
2548
+ handleSuspenseTaskResult(res, push, nonceAttr);
2419
2549
  }
2420
2550
  controller.close();
2421
2551
  }
@@ -2428,12 +2558,10 @@ function $RC(id, templateId) {
2428
2558
  },
2429
2559
  });
2430
2560
  };
2431
- const renderToString = (element, options = {}) => {
2561
+ const renderToString = (element, _options = {}) => {
2432
2562
  const state = getState();
2433
2563
  const wasServerRendering = state.isServerRendering;
2434
- state.isServerRendering = true;
2435
- state.ssrMetadata = {};
2436
- resetIdCounter();
2564
+ beginSsrRender();
2437
2565
  try {
2438
2566
  return renderToStringImpl(element);
2439
2567
  }
@@ -2587,7 +2715,9 @@ function $RC(id, templateId) {
2587
2715
  if (anyPending && !getState().isSuspenseBackground) {
2588
2716
  return fallback || null;
2589
2717
  }
2590
- return createElement(Fragment, { children });
2718
+ return createElement(Fragment, {
2719
+ children: children,
2720
+ });
2591
2721
  };
2592
2722
  Suspense.type = RYUNIX_TYPES.RYUNIX_SUSPENSE;
2593
2723
  function preload(importFn) {
@@ -2597,6 +2727,7 @@ function $RC(id, templateId) {
2597
2727
  var index = /*#__PURE__*/Object.freeze({
2598
2728
  __proto__: null,
2599
2729
  Children: Children,
2730
+ INTERNAL_META_KEYS: INTERNAL_META_KEYS,
2600
2731
  Link: Link,
2601
2732
  NavLink: NavLink,
2602
2733
  RouterProvider: RouterProvider,
@@ -2606,8 +2737,10 @@ function $RC(id, templateId) {
2606
2737
  forwardRef: forwardRef,
2607
2738
  lazy: lazy,
2608
2739
  memo: memo,
2740
+ mergeRouteMetadata: mergeRouteMetadata,
2609
2741
  preload: preload,
2610
2742
  resetIdCounter: resetIdCounter,
2743
+ resolvePageMetadata: resolvePageMetadata,
2611
2744
  shallowEqual: shallowEqual,
2612
2745
  useCallback: useCallback,
2613
2746
  useDebounce: useDebounce,
@@ -2754,11 +2887,11 @@ function $RC(id, templateId) {
2754
2887
  return Profiled;
2755
2888
  }
2756
2889
 
2757
- function ServerBoundary({ children, id }) {
2890
+ function ServerBoundary({ children, id, }) {
2758
2891
  return createElement('div', { 'data-ryunix-server': id, style: { display: 'contents' } }, children);
2759
2892
  }
2760
2893
  ServerBoundary.ryunix_type = 'RYUNIX_SERVER_BOUNDARY';
2761
- function HydrationBoundary({ children, id }) {
2894
+ function HydrationBoundary({ children, id, }) {
2762
2895
  return createElement('div', {
2763
2896
  'data-ryunix-hydrate-boundary': id ?? '',
2764
2897
  suppressHydrationWarning: true,
@@ -2793,7 +2926,7 @@ function $RC(id, templateId) {
2793
2926
  body: JSON.stringify({ actionId, args }),
2794
2927
  });
2795
2928
  if (!response.ok) {
2796
- const errorData = await response.json().catch(() => ({}));
2929
+ const errorData = (await response.json().catch(() => ({})));
2797
2930
  throw new Error(errorData.error || 'Server Action failed');
2798
2931
  }
2799
2932
  return response.json();
@@ -2819,9 +2952,7 @@ function $RC(id, templateId) {
2819
2952
  else if ('error' in rawError) {
2820
2953
  const nested = rawError.error;
2821
2954
  error =
2822
- nested && typeof nested === 'object'
2823
- ? nested
2824
- : null;
2955
+ nested && typeof nested === 'object' ? nested : null;
2825
2956
  }
2826
2957
  else {
2827
2958
  error = rawError;
@@ -3159,9 +3290,7 @@ function $RC(id, templateId) {
3159
3290
  if (parts.length >= 2) {
3160
3291
  const lastPart = parts[parts.length - 1];
3161
3292
  const colonIdx = lastPart.indexOf(':');
3162
- const fileCandidate = colonIdx > 0
3163
- ? lastPart.slice(0, colonIdx)
3164
- : lastPart;
3293
+ const fileCandidate = colonIdx > 0 ? lastPart.slice(0, colonIdx) : lastPart;
3165
3294
  if (exts.some((ext) => fileCandidate.endsWith(ext))) {
3166
3295
  fnName = parts.slice(0, -1).join(' ');
3167
3296
  filePath = lastPart;
@@ -3448,9 +3577,243 @@ function $RC(id, templateId) {
3448
3577
  : null, bottomBar));
3449
3578
  }
3450
3579
 
3580
+ function getRyunixI18nConfig() {
3581
+ try {
3582
+ const value = ryunix.config.i18n;
3583
+ if (value && Array.isArray(value.locales) && value.locales.length > 0) {
3584
+ return value;
3585
+ }
3586
+ }
3587
+ catch {
3588
+ }
3589
+ return null;
3590
+ }
3591
+
3592
+ function defineMessages(messages) {
3593
+ return messages;
3594
+ }
3595
+ function resolveMessageKey(tree, key) {
3596
+ if (!tree)
3597
+ return undefined;
3598
+ const parts = key.split('.');
3599
+ let node = tree;
3600
+ for (const part of parts) {
3601
+ if (node == null || typeof node === 'string')
3602
+ return undefined;
3603
+ node = node[part];
3604
+ }
3605
+ return typeof node === 'string' ? node : undefined;
3606
+ }
3607
+ function formatMessage(template, params) {
3608
+ if (!params)
3609
+ return template;
3610
+ return Object.entries(params).reduce((value, [name, replacement]) => value.replace(new RegExp(`\\{${escapeRegExp$2(name)}\\}`, 'g'), String(replacement)), template);
3611
+ }
3612
+ function escapeRegExp$2(value) {
3613
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3614
+ }
3615
+
3616
+ function normalizeI18nConfig(config) {
3617
+ const locales = [...new Set(config.locales.filter(Boolean))];
3618
+ if (locales.length === 0) {
3619
+ throw new Error('[Ryunix i18n] At least one locale is required.');
3620
+ }
3621
+ const defaultLocale = locales.includes(config.defaultLocale)
3622
+ ? config.defaultLocale
3623
+ : locales[0];
3624
+ return { ...config, locales, defaultLocale };
3625
+ }
3626
+ function pickLocale(record, locale, fallback) {
3627
+ return record?.[locale] ?? record?.[fallback];
3628
+ }
3629
+ function getLocaleFromPath(pathname, locales) {
3630
+ if (typeof pathname !== 'string' || locales.length === 0)
3631
+ return null;
3632
+ const pattern = new RegExp(`^/(${locales.map(escapeRegExp$1).join('|')})(/|$)`);
3633
+ const match = pathname.match(pattern);
3634
+ return match ? match[1] : null;
3635
+ }
3636
+ function localePath(locale, path = '', locales) {
3637
+ const normalized = path.startsWith('/') ? path : path ? `/${path}` : '';
3638
+ if (!locales.includes(locale))
3639
+ return normalized || '/';
3640
+ if (!normalized || normalized === '/')
3641
+ return `/${locale}`;
3642
+ return `/${locale}${normalized}`;
3643
+ }
3644
+ function swapLocalePath(pathname, targetLocale, options) {
3645
+ const { locales } = options;
3646
+ if (!locales.includes(targetLocale))
3647
+ return pathname;
3648
+ const current = getLocaleFromPath(pathname, locales);
3649
+ if (current) {
3650
+ const rest = pathname.slice(current.length + 1) || '';
3651
+ return localePath(targetLocale, rest, locales);
3652
+ }
3653
+ if (pathname.startsWith('/')) {
3654
+ return localePath(targetLocale, pathname, locales);
3655
+ }
3656
+ return localePath(targetLocale, '', locales);
3657
+ }
3658
+ function escapeRegExp$1(value) {
3659
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3660
+ }
3661
+
3662
+ const DEFAULT_LOCALE_COOKIE_NAME = 'ryunix_locale';
3663
+ function resolveCookieOptions(cookie, cookieName, maxAgeSeconds) {
3664
+ const enabled = cookie !== false;
3665
+ const cookieConfig = typeof cookie === 'object' ? cookie : {};
3666
+ return {
3667
+ enabled,
3668
+ cookieName: cookieName ?? cookieConfig.name ?? DEFAULT_LOCALE_COOKIE_NAME,
3669
+ maxAgeSeconds: maxAgeSeconds ?? cookieConfig.maxAgeSeconds ?? 365 * 24 * 60 * 60,
3670
+ };
3671
+ }
3672
+ function normalizeOptions(options) {
3673
+ const base = normalizeI18nConfig({
3674
+ locales: options.locales,
3675
+ defaultLocale: options.defaultLocale,
3676
+ });
3677
+ const cookie = resolveCookieOptions(options.cookie, options.cookieName, options.maxAgeSeconds);
3678
+ const localeLabels = { ...options.localeLabels };
3679
+ for (const locale of base.locales) {
3680
+ if (!localeLabels[locale])
3681
+ localeLabels[locale] = locale.toUpperCase();
3682
+ }
3683
+ return {
3684
+ locales: base.locales,
3685
+ defaultLocale: base.defaultLocale,
3686
+ localeLabels,
3687
+ cookieName: cookie.cookieName,
3688
+ maxAgeSeconds: cookie.maxAgeSeconds,
3689
+ cookieEnabled: cookie.enabled,
3690
+ messages: options.messages ?? {},
3691
+ };
3692
+ }
3693
+ function createTranslate(messages, locale, defaultLocale) {
3694
+ return (key, params) => {
3695
+ const value = resolveMessageKey(messages[locale], key) ??
3696
+ resolveMessageKey(messages[defaultLocale], key) ??
3697
+ key;
3698
+ return formatMessage(value, params);
3699
+ };
3700
+ }
3701
+ function createI18n(options) {
3702
+ const config = normalizeOptions(options);
3703
+ const { Provider: I18nContextProvider, useContext } = createContext('ryunix.i18n', {
3704
+ locale: config.defaultLocale,
3705
+ defaultLocale: config.defaultLocale,
3706
+ locales: config.locales,
3707
+ localeLabels: config.localeLabels,
3708
+ t: (key) => key,
3709
+ });
3710
+ const isLocale = (value) => typeof value === 'string' && config.locales.includes(value);
3711
+ const getLocaleCookie = () => {
3712
+ if (!config.cookieEnabled || typeof document === 'undefined')
3713
+ return null;
3714
+ const pattern = new RegExp(`(?:^|;\\s*)${config.cookieName}=(${config.locales.map(escapeRegExp).join('|')})(?:;|$)`);
3715
+ const match = document.cookie.match(pattern);
3716
+ return match ? match[1] : null;
3717
+ };
3718
+ const setLocaleCookie = (locale) => {
3719
+ if (!config.cookieEnabled ||
3720
+ typeof document === 'undefined' ||
3721
+ !isLocale(locale))
3722
+ return;
3723
+ document.cookie = `${config.cookieName}=${locale}; path=/; max-age=${config.maxAgeSeconds}; SameSite=Lax`;
3724
+ };
3725
+ const resolveLocaleFromCookie = () => getLocaleCookie() || config.defaultLocale;
3726
+ const getLocaleRedirectScript = () => {
3727
+ if (!config.cookieEnabled)
3728
+ return '';
3729
+ const localesPattern = config.locales.map(escapeRegExp).join('|');
3730
+ return `(function(){try{var m=document.cookie.match(/(?:^|;\\s*)${config.cookieName}=(${localesPattern})(?:;|$)/);var l=m?m[1]:'${config.defaultLocale}';var p=location.pathname;var r=new RegExp('^/(${localesPattern})(/|$)');if(!r.test(p)){location.replace('/'+l+(p==='/'?'':p));}}catch(e){}})();`;
3731
+ };
3732
+ const Provider = ({ locale, children, }) => {
3733
+ const resolved = isLocale(locale) ? locale : config.defaultLocale;
3734
+ const value = {
3735
+ locale: resolved,
3736
+ defaultLocale: config.defaultLocale,
3737
+ locales: config.locales,
3738
+ localeLabels: config.localeLabels,
3739
+ t: createTranslate(config.messages, resolved, config.defaultLocale),
3740
+ };
3741
+ return createElement(I18nContextProvider, { value, children });
3742
+ };
3743
+ const useI18n = () => useContext();
3744
+ const useLocale = () => useI18n().locale;
3745
+ const useTranslations = () => useI18n().t;
3746
+ const generateStaticParams = () => config.locales.map((locale) => ({ locale }));
3747
+ const LocaleSwitcher = ({ className = '' }) => {
3748
+ const { location, navigate } = useRouter();
3749
+ const { locale: current, localeLabels, locales } = useI18n();
3750
+ const onSelect = (locale) => {
3751
+ if (locale === current)
3752
+ return;
3753
+ setLocaleCookie(locale);
3754
+ const next = swapLocalePath(location, locale, {
3755
+ locales,
3756
+ defaultLocale: config.defaultLocale,
3757
+ });
3758
+ navigate(next.startsWith(`/${locale}`) ? next : `/${locale}`);
3759
+ };
3760
+ return createElement('div', {
3761
+ className: `ryunix-locale-switcher ${className}`.trim(),
3762
+ role: 'group',
3763
+ 'aria-label': 'Language',
3764
+ children: locales.map((locale) => createElement('button', {
3765
+ key: locale,
3766
+ type: 'button',
3767
+ onClick: () => onSelect(locale),
3768
+ className: locale === current
3769
+ ? 'ryunix-locale-switcher__btn is-active'
3770
+ : 'ryunix-locale-switcher__btn',
3771
+ 'aria-current': locale === current ? 'true' : undefined,
3772
+ }, localeLabels[locale] ?? locale)),
3773
+ });
3774
+ };
3775
+ return {
3776
+ config,
3777
+ Provider,
3778
+ useI18n,
3779
+ useLocale,
3780
+ useTranslations,
3781
+ LocaleSwitcher,
3782
+ generateStaticParams,
3783
+ getLocaleFromPath: (pathname) => getLocaleFromPath(pathname, config.locales),
3784
+ localePath: (locale, path = '') => localePath(locale, path, config.locales),
3785
+ swapLocalePath: (pathname, targetLocale) => swapLocalePath(pathname, targetLocale, {
3786
+ locales: config.locales,
3787
+ defaultLocale: config.defaultLocale,
3788
+ }),
3789
+ pickLocale: (record, locale) => pickLocale(record, locale, config.defaultLocale),
3790
+ getLocaleCookie,
3791
+ setLocaleCookie,
3792
+ resolveLocaleFromCookie,
3793
+ getLocaleRedirectScript,
3794
+ };
3795
+ }
3796
+ function createAppI18n(messages, fallback) {
3797
+ const fromConfig = getRyunixI18nConfig() ?? fallback;
3798
+ if (!fromConfig) {
3799
+ throw new Error('[Ryunix i18n] Missing ryunix.config.i18n. Add i18n: { locales, defaultLocale } to ryunix.config.js, pass a fallback to createAppI18n(), or call createI18n() directly.');
3800
+ }
3801
+ return createI18n({ ...fromConfig, messages });
3802
+ }
3803
+ function createI18nFromConfig(config, messages) {
3804
+ if (!config?.locales?.length) {
3805
+ throw new Error('[Ryunix i18n] Invalid i18n config: locales array is required.');
3806
+ }
3807
+ return createI18n({ ...config, messages });
3808
+ }
3809
+ function escapeRegExp(value) {
3810
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3811
+ }
3812
+
3451
3813
  var Ryunix = /*#__PURE__*/Object.freeze({
3452
3814
  __proto__: null,
3453
3815
  Children: Children,
3816
+ DEFAULT_LOCALE_COOKIE_NAME: DEFAULT_LOCALE_COOKIE_NAME,
3454
3817
  DEFAULT_THEME_COOKIE_NAME: DEFAULT_THEME_COOKIE_NAME,
3455
3818
  ErrorBoundary: ErrorBoundary,
3456
3819
  Footer: Footer,
@@ -3458,6 +3821,7 @@ function $RC(id, templateId) {
3458
3821
  Header: Header,
3459
3822
  Hooks: index,
3460
3823
  HydrationBoundary: HydrationBoundary,
3824
+ INTERNAL_META_KEYS: INTERNAL_META_KEYS,
3461
3825
  Link: Link,
3462
3826
  Main: Main,
3463
3827
  NavLink: NavLink,
@@ -3473,13 +3837,19 @@ function $RC(id, templateId) {
3473
3837
  batchUpdates: batchUpdates,
3474
3838
  cloneElement: cloneElement,
3475
3839
  createActionProxy: createActionProxy,
3840
+ createAppI18n: createAppI18n,
3476
3841
  createContext: createContext,
3477
3842
  createElement: createElement,
3843
+ createI18n: createI18n,
3844
+ createI18nFromConfig: createI18nFromConfig,
3478
3845
  createPortal: createPortal,
3479
3846
  createThemeController: createThemeController,
3480
3847
  deepEqual: deepEqual,
3848
+ defineMessages: defineMessages,
3481
3849
  escapeHtml: escapeHtml,
3482
3850
  forwardRef: forwardRef,
3851
+ getLocaleFromPath: getLocaleFromPath,
3852
+ getRyunixI18nConfig: getRyunixI18nConfig,
3483
3853
  getState: getState,
3484
3854
  getSystemColorScheme: getSystemColorScheme,
3485
3855
  getThemeCookie: getThemeCookie,
@@ -3487,12 +3857,16 @@ function $RC(id, templateId) {
3487
3857
  init: init,
3488
3858
  isValidElement: isValidElement,
3489
3859
  lazy: lazy,
3860
+ localePath: localePath,
3490
3861
  logHydrationBoundaryMismatch: logHydrationBoundaryMismatch,
3491
3862
  logHydrationBoundaryRecovery: logHydrationBoundaryRecovery,
3492
3863
  logHydrationFatal: logHydrationFatal,
3493
3864
  logHydrationInfo: logHydrationInfo,
3494
3865
  logHydrationRecoverable: logHydrationRecoverable,
3495
3866
  memo: memo,
3867
+ mergeRouteMetadata: mergeRouteMetadata,
3868
+ normalizeI18nConfig: normalizeI18nConfig,
3869
+ pickLocale: pickLocale,
3496
3870
  preload: preload,
3497
3871
  profiler: profiler,
3498
3872
  render: render,
@@ -3501,10 +3875,12 @@ function $RC(id, templateId) {
3501
3875
  renderToStringAsync: renderToStringAsync,
3502
3876
  resetIdCounter: resetIdCounter,
3503
3877
  resolveEffectiveTheme: resolveEffectiveTheme,
3878
+ resolvePageMetadata: resolvePageMetadata,
3504
3879
  resolveThemeFromCookie: resolveThemeFromCookie,
3505
3880
  safeRender: safeRender,
3506
3881
  setThemeCookie: setThemeCookie,
3507
3882
  shallowEqual: shallowEqual,
3883
+ swapLocalePath: swapLocalePath,
3508
3884
  themeController: themeController,
3509
3885
  themeInitScript: themeInitScript,
3510
3886
  useCallback: useCallback,
@@ -3596,7 +3972,7 @@ function $RC(id, templateId) {
3596
3972
  const Image = ({ src, ...props }) => {
3597
3973
  return createElement('img', { ...props, src });
3598
3974
  };
3599
- const MDXContent = ({ children, components = {} }) => {
3975
+ const MDXContent = ({ children, components = {}, }) => {
3600
3976
  const mergedComponents = getMDXComponents(components);
3601
3977
  return createElement(MDXProvider, { value: mergedComponents }, createElement('div', null, children));
3602
3978
  };
@@ -3605,6 +3981,7 @@ function $RC(id, templateId) {
3605
3981
  window.Ryunix = Ryunix;
3606
3982
 
3607
3983
  exports.Children = Children;
3984
+ exports.DEFAULT_LOCALE_COOKIE_NAME = DEFAULT_LOCALE_COOKIE_NAME;
3608
3985
  exports.DEFAULT_THEME_COOKIE_NAME = DEFAULT_THEME_COOKIE_NAME;
3609
3986
  exports.ErrorBoundary = ErrorBoundary;
3610
3987
  exports.Footer = Footer;
@@ -3612,6 +3989,7 @@ function $RC(id, templateId) {
3612
3989
  exports.Header = Header;
3613
3990
  exports.Hooks = index;
3614
3991
  exports.HydrationBoundary = HydrationBoundary;
3992
+ exports.INTERNAL_META_KEYS = INTERNAL_META_KEYS;
3615
3993
  exports.Image = Image;
3616
3994
  exports.Link = Link;
3617
3995
  exports.MDXContent = MDXContent;
@@ -3630,16 +4008,22 @@ function $RC(id, templateId) {
3630
4008
  exports.batchUpdates = batchUpdates;
3631
4009
  exports.cloneElement = cloneElement;
3632
4010
  exports.createActionProxy = createActionProxy;
4011
+ exports.createAppI18n = createAppI18n;
3633
4012
  exports.createContext = createContext;
3634
4013
  exports.createElement = createElement;
4014
+ exports.createI18n = createI18n;
4015
+ exports.createI18nFromConfig = createI18nFromConfig;
3635
4016
  exports.createPortal = createPortal;
3636
4017
  exports.createThemeController = createThemeController;
3637
4018
  exports.deepEqual = deepEqual;
3638
4019
  exports.default = Ryunix;
3639
4020
  exports.defaultComponents = defaultComponents;
4021
+ exports.defineMessages = defineMessages;
3640
4022
  exports.escapeHtml = escapeHtml;
3641
4023
  exports.forwardRef = forwardRef;
4024
+ exports.getLocaleFromPath = getLocaleFromPath;
3642
4025
  exports.getMDXComponents = getMDXComponents;
4026
+ exports.getRyunixI18nConfig = getRyunixI18nConfig;
3643
4027
  exports.getState = getState;
3644
4028
  exports.getSystemColorScheme = getSystemColorScheme;
3645
4029
  exports.getThemeCookie = getThemeCookie;
@@ -3647,12 +4031,16 @@ function $RC(id, templateId) {
3647
4031
  exports.init = init;
3648
4032
  exports.isValidElement = isValidElement;
3649
4033
  exports.lazy = lazy;
4034
+ exports.localePath = localePath;
3650
4035
  exports.logHydrationBoundaryMismatch = logHydrationBoundaryMismatch;
3651
4036
  exports.logHydrationBoundaryRecovery = logHydrationBoundaryRecovery;
3652
4037
  exports.logHydrationFatal = logHydrationFatal;
3653
4038
  exports.logHydrationInfo = logHydrationInfo;
3654
4039
  exports.logHydrationRecoverable = logHydrationRecoverable;
3655
4040
  exports.memo = memo;
4041
+ exports.mergeRouteMetadata = mergeRouteMetadata;
4042
+ exports.normalizeI18nConfig = normalizeI18nConfig;
4043
+ exports.pickLocale = pickLocale;
3656
4044
  exports.preload = preload;
3657
4045
  exports.profiler = profiler;
3658
4046
  exports.render = render;
@@ -3661,11 +4049,13 @@ function $RC(id, templateId) {
3661
4049
  exports.renderToStringAsync = renderToStringAsync;
3662
4050
  exports.resetIdCounter = resetIdCounter;
3663
4051
  exports.resolveEffectiveTheme = resolveEffectiveTheme;
4052
+ exports.resolvePageMetadata = resolvePageMetadata;
3664
4053
  exports.resolveThemeFromCookie = resolveThemeFromCookie;
3665
4054
  exports.ryxProps = ryxProps;
3666
4055
  exports.safeRender = safeRender;
3667
4056
  exports.setThemeCookie = setThemeCookie;
3668
4057
  exports.shallowEqual = shallowEqual;
4058
+ exports.swapLocalePath = swapLocalePath;
3669
4059
  exports.themeController = themeController;
3670
4060
  exports.themeInitScript = themeInitScript;
3671
4061
  exports.useCallback = useCallback;