@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.
@@ -426,6 +426,7 @@ const updateDom = (dom, prevProps = {}, nextProps = {}) => {
426
426
  const el = dom;
427
427
  const domEl = el;
428
428
  const handlerMap = domEl._ryunixHandlers;
429
+ const elRecord = el;
429
430
  Object.keys(prevProps)
430
431
  .filter(isEvent)
431
432
  .filter((key) => isGone(nextProps)(key) || isNew(prevProps, nextProps)(key))
@@ -460,7 +461,7 @@ const updateDom = (dom, prevProps = {}, nextProps = {}) => {
460
461
  el.removeAttribute(attrName);
461
462
  }
462
463
  else {
463
- el[propKey] = '';
464
+ elRecord[propKey] = '';
464
465
  el.removeAttribute(propKey);
465
466
  }
466
467
  });
@@ -481,8 +482,8 @@ const updateDom = (dom, prevProps = {}, nextProps = {}) => {
481
482
  }
482
483
  else {
483
484
  if (propKey === 'value' || propKey === 'checked') {
484
- if (el[propKey] !== nextProps[propKey]) {
485
- el[propKey] = nextProps[propKey];
485
+ if (elRecord[propKey] !== nextProps[propKey]) {
486
+ elRecord[propKey] = nextProps[propKey];
486
487
  }
487
488
  }
488
489
  else {
@@ -490,15 +491,15 @@ const updateDom = (dom, prevProps = {}, nextProps = {}) => {
490
491
  if (isSvgNode) {
491
492
  const attrName = toSvgAttrName(propKey);
492
493
  const svgValidated = checkAttributeUri(attrName, nextProps[propKey]);
493
- el.setAttribute(attrName, svgValidated);
494
+ el.setAttribute(attrName, String(svgValidated));
494
495
  }
495
496
  else {
496
497
  const attrVal = nextProps[propKey];
497
498
  const safeValue = checkAttributeUri(propKey, attrVal);
498
- el[propKey] = safeValue;
499
+ elRecord[propKey] = safeValue;
499
500
  if (typeof attrVal !== 'object' &&
500
501
  typeof attrVal !== 'function') {
501
- el.setAttribute(propKey, safeValue);
502
+ el.setAttribute(propKey, String(safeValue));
502
503
  }
503
504
  }
504
505
  }
@@ -665,9 +666,7 @@ const runLayoutEffects = (fiber) => {
665
666
  }
666
667
  try {
667
668
  const cleanup = hook.effect();
668
- hook.cancel = is.function(cleanup)
669
- ? cleanup
670
- : null;
669
+ hook.cancel = is.function(cleanup) ? cleanup : null;
671
670
  }
672
671
  catch (error) {
673
672
  if (process.env.NODE_ENV !== 'production') {
@@ -699,9 +698,7 @@ const runNormalEffects = (fiber) => {
699
698
  }
700
699
  try {
701
700
  const cleanup = hook.effect();
702
- hook.cancel = is.function(cleanup)
703
- ? cleanup
704
- : null;
701
+ hook.cancel = is.function(cleanup) ? cleanup : null;
705
702
  }
706
703
  catch (error) {
707
704
  if (process.env.NODE_ENV !== 'production') {
@@ -717,6 +714,8 @@ function commitRoot() {
717
714
  const state = getState();
718
715
  state.deletions.forEach(commitWork);
719
716
  const finishedWork = state.wipRoot;
717
+ if (!finishedWork)
718
+ return;
720
719
  state.currentRoot = finishedWork;
721
720
  if (state.isHydrating || state.hydrationFailed) {
722
721
  if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
@@ -773,6 +772,8 @@ function commitWork(fiber) {
773
772
  return;
774
773
  }
775
774
  const domParent = domParentFiber.dom;
775
+ if (!domParent)
776
+ return;
776
777
  if (fiber.effectTag === EFFECT_TAGS.PLACEMENT) {
777
778
  if (fiber.dom != null) {
778
779
  if (process.env.NODE_ENV !== 'production' && process.env.RYUNIX_DEBUG) {
@@ -786,7 +787,7 @@ function commitWork(fiber) {
786
787
  else if (fiber.effectTag === EFFECT_TAGS.UPDATE) {
787
788
  cancelEffects(fiber);
788
789
  if (fiber.dom != null) {
789
- updateDom(fiber.dom, fiber.alternate.props, fiber.props);
790
+ updateDom(fiber.dom, fiber.alternate?.props, fiber.props);
790
791
  }
791
792
  runLayoutEffects(fiber);
792
793
  runNormalEffects(fiber);
@@ -835,7 +836,7 @@ const commitPortalWork = (fiber, portalContainer) => {
835
836
  else if (fiber.effectTag === EFFECT_TAGS.UPDATE) {
836
837
  cancelEffects(fiber);
837
838
  if (fiber.dom != null) {
838
- updateDom(fiber.dom, fiber.alternate.props, fiber.props);
839
+ updateDom(fiber.dom, fiber.alternate?.props, fiber.props);
839
840
  }
840
841
  runLayoutEffects(fiber);
841
842
  runNormalEffects(fiber);
@@ -888,6 +889,12 @@ const reconcileChildren = (wipFiber, elements) => {
888
889
  let newFiber;
889
890
  const sameType = matchedFiber && element.type === matchedFiber.type;
890
891
  if (sameType && matchedFiber) {
892
+ const matchedType = matchedFiber.type;
893
+ const isErrorBoundary = typeof matchedFiber.type === 'function' &&
894
+ matchedType?.ryunix_type === 'RYUNIX_ERROR_BOUNDARY';
895
+ const preserveBoundaryError = isErrorBoundary &&
896
+ matchedFiber.stateError != null &&
897
+ matchedFiber.child == null;
891
898
  newFiber = {
892
899
  type: matchedFiber.type,
893
900
  props: element.props,
@@ -896,7 +903,7 @@ const reconcileChildren = (wipFiber, elements) => {
896
903
  alternate: matchedFiber,
897
904
  effectTag: EFFECT_TAGS.UPDATE,
898
905
  hooks: matchedFiber.hooks,
899
- stateError: matchedFiber.stateError,
906
+ stateError: preserveBoundaryError ? matchedFiber.stateError : undefined,
900
907
  key: element.key,
901
908
  index,
902
909
  };
@@ -964,8 +971,7 @@ const findNearestHydrationBoundary = (fiber) => {
964
971
  const maybeTyped = type;
965
972
  if (type &&
966
973
  typeof type === 'function' &&
967
- (maybeTyped?.ryunix_type === 'RYUNIX_HYDRATION_BOUNDARY' ||
968
- maybeTyped?.ryunix_type === 'RYUNIX_SERVER_BOUNDARY')) {
974
+ maybeTyped?.ryunix_type === 'RYUNIX_HYDRATION_BOUNDARY') {
969
975
  return current;
970
976
  }
971
977
  current = current.parent || null;
@@ -1031,11 +1037,15 @@ const updateFunctionComponent = (fiber) => {
1031
1037
  const state = getState();
1032
1038
  state.wipFiber = fiber;
1033
1039
  state.hookIndex = 0;
1034
- state.wipFiber.hooks = [];
1040
+ fiber.hooks = [];
1041
+ const componentType = fiber.type;
1042
+ if (state.hydrationRecover &&
1043
+ componentType.ryunix_type === 'RYUNIX_ERROR_BOUNDARY') {
1044
+ fiber.stateError = undefined;
1045
+ }
1035
1046
  if (state.isHydrating) {
1036
1047
  fiber.effectTag = EFFECT_TAGS.HYDRATE;
1037
1048
  }
1038
- const componentType = fiber.type;
1039
1049
  if (componentType._isMemo && fiber.alternate) {
1040
1050
  const { children: _pc, ...prevRest } = fiber.alternate.props || {};
1041
1051
  const { children: _nc, ...nextRest } = fiber.props || {};
@@ -1049,7 +1059,7 @@ const updateFunctionComponent = (fiber) => {
1049
1059
  return;
1050
1060
  }
1051
1061
  }
1052
- let children = [
1062
+ const children = [
1053
1063
  componentType(fiber.props),
1054
1064
  ];
1055
1065
  if (componentType._contextId && fiber.props?.value !== undefined) {
@@ -1067,11 +1077,24 @@ const isUnderClientOnlyBoundary = (fiber) => {
1067
1077
  }
1068
1078
  return false;
1069
1079
  };
1080
+ const isUnderServerPreserveBoundary = (fiber) => {
1081
+ let current = fiber?.parent || null;
1082
+ while (current) {
1083
+ if (current._hydratePreserveServer)
1084
+ return true;
1085
+ current = current.parent || null;
1086
+ }
1087
+ return false;
1088
+ };
1089
+ const normalizeChildNodes = (children) => {
1090
+ if (children == null)
1091
+ return [];
1092
+ return Array.isArray(children) ? children : [children];
1093
+ };
1070
1094
  const updateHostComponent = (fiber) => {
1071
1095
  const state = getState();
1072
1096
  if (fiber.type === RYUNIX_TYPES.RYUNIX_CONTEXT) {
1073
- fiber._contextId =
1074
- fiber.props?._contextId;
1097
+ fiber._contextId = fiber.props?._contextId;
1075
1098
  fiber._contextValue = fiber.props?.value;
1076
1099
  }
1077
1100
  const isPassthrough = fiber.type === RYUNIX_TYPES.RYUNIX_FRAGMENT ||
@@ -1080,6 +1103,9 @@ const updateHostComponent = (fiber) => {
1080
1103
  if (state.isHydrating && isPassthrough) {
1081
1104
  fiber.effectTag = EFFECT_TAGS.HYDRATE;
1082
1105
  }
1106
+ else if (state.isHydrating && isUnderServerPreserveBoundary(fiber)) {
1107
+ return;
1108
+ }
1083
1109
  else if (state.isHydrating && isUnderClientOnlyBoundary(fiber)) {
1084
1110
  if (!fiber.dom) {
1085
1111
  fiber.dom = createDom(fiber);
@@ -1102,6 +1128,13 @@ const updateHostComponent = (fiber) => {
1102
1128
  domNode.nodeValue = String(fiber.props.nodeValue);
1103
1129
  logHydrationRecoverable('text');
1104
1130
  }
1131
+ if (isElement &&
1132
+ domNode.hasAttribute('data-ryunix-server')) {
1133
+ fiber._hydratePreserveServer = true;
1134
+ state.hydrateCursor = nextValidSibling$1(domNode);
1135
+ reconcileChildren(fiber, []);
1136
+ return;
1137
+ }
1105
1138
  if (isElement &&
1106
1139
  domNode.hasAttribute('data-ryunix-hydrate-boundary')) {
1107
1140
  fiber._hydrateClientOnly = true;
@@ -1110,7 +1143,7 @@ const updateHostComponent = (fiber) => {
1110
1143
  }
1111
1144
  else {
1112
1145
  const policy = getHydrationPolicy();
1113
- const detail = `Mismatch at ${getTypeLabel(fiber.type)}. Expected ${domNode.nodeType === 1 ? domNode.tagName : 'text'} but got ${String(fiber.type)}.`;
1146
+ const detail = `Mismatch at ${getTypeLabel(fiber.type ?? 'unknown')}. Expected ${domNode.nodeType === 1 ? domNode.tagName : 'text'} but got ${String(fiber.type)}.`;
1114
1147
  const boundaryFiber = findNearestHydrationBoundary(fiber);
1115
1148
  const boundaryDom = (boundaryFiber ? getBoundaryDom(boundaryFiber) : null) ??
1116
1149
  findBoundaryDomFromNode(state.hydrateCursor);
@@ -1142,7 +1175,10 @@ const updateHostComponent = (fiber) => {
1142
1175
  fiber.dom = createDom(fiber);
1143
1176
  }
1144
1177
  }
1145
- const children = fiber.props?.children || [];
1178
+ if (fiber._hydratePreserveServer) {
1179
+ return;
1180
+ }
1181
+ const children = normalizeChildNodes(fiber.props?.children);
1146
1182
  reconcileChildren(fiber, children);
1147
1183
  };
1148
1184
  const getTypeLabel = (type) => {
@@ -1166,6 +1202,11 @@ const scheduleWork$1 = (root, priority) => {
1166
1202
  }
1167
1203
  };
1168
1204
 
1205
+ const getRootChild = (children) => {
1206
+ if (children == null)
1207
+ return undefined;
1208
+ return Array.isArray(children) ? children[0] : children;
1209
+ };
1169
1210
  const renderSubtree = (element, container) => {
1170
1211
  clearContainer(container);
1171
1212
  const root = {
@@ -1195,7 +1236,7 @@ const recoverHydrationFailureIfNeeded = () => {
1195
1236
  if (policy.recover === 'none')
1196
1237
  return;
1197
1238
  const container = state.containerRoot || state.currentRoot?.dom;
1198
- const element = state.currentRoot?.props?.children?.[0];
1239
+ const element = getRootChild(state.currentRoot?.props?.children);
1199
1240
  if (!container || element == null)
1200
1241
  return;
1201
1242
  state.hydrationRecover = true;
@@ -1205,8 +1246,10 @@ const recoverHydrationFailureIfNeeded = () => {
1205
1246
  renderSubtree(element, container);
1206
1247
  };
1207
1248
  const runHydrationRecovery = () => {
1249
+ const state = getState();
1208
1250
  recoverScopedHydrationFailures();
1209
1251
  recoverHydrationFailureIfNeeded();
1252
+ state.hydrationRecover = false;
1210
1253
  };
1211
1254
 
1212
1255
  let workQueue = [];
@@ -1226,32 +1269,36 @@ function performUnitOfWork(fiber) {
1226
1269
  if (process.env.NODE_ENV !== 'production') {
1227
1270
  console.error('[Ryunix ErrorBoundary] Caught error during render:', error);
1228
1271
  try {
1229
- const src = fiber.props && fiber.props.__source;
1272
+ const fiberProps = fiber.props;
1273
+ const src = fiberProps?.__source;
1230
1274
  if (src && error && typeof error === 'object') {
1275
+ ;
1231
1276
  error.__ryunix_source = src;
1232
1277
  }
1233
1278
  let targetFiber = fiber;
1234
1279
  while (!error.__ryunix_source && targetFiber) {
1235
- if (targetFiber.props && targetFiber.props.__source) {
1236
- error.__ryunix_source = targetFiber.props.__source;
1280
+ const targetProps = targetFiber.props;
1281
+ if (targetProps?.__source) {
1282
+ ;
1283
+ error.__ryunix_source = targetProps.__source;
1237
1284
  }
1238
1285
  targetFiber = targetFiber.parent;
1239
1286
  }
1240
1287
  }
1241
- catch (e) { }
1288
+ catch (_e) { }
1242
1289
  }
1243
1290
  let boundaryFiber = fiber.parent;
1244
1291
  let foundBoundary = false;
1245
1292
  while (boundaryFiber) {
1246
1293
  if (boundaryFiber.type &&
1247
- boundaryFiber.type
1248
- .ryunix_type === 'RYUNIX_ERROR_BOUNDARY') {
1294
+ boundaryFiber.type.ryunix_type ===
1295
+ 'RYUNIX_ERROR_BOUNDARY') {
1249
1296
  foundBoundary = true;
1250
1297
  break;
1251
1298
  }
1252
1299
  boundaryFiber = boundaryFiber.parent;
1253
1300
  }
1254
- if (foundBoundary) {
1301
+ if (foundBoundary && boundaryFiber) {
1255
1302
  if (process.env.NODE_ENV !== 'production') {
1256
1303
  console.warn('[Ryunix ErrorBoundary] Recovering tree at nearest boundary.');
1257
1304
  }
@@ -1278,6 +1325,7 @@ function performUnitOfWork(fiber) {
1278
1325
  }
1279
1326
  nextFiber = nextFiber.parent;
1280
1327
  }
1328
+ return null;
1281
1329
  }
1282
1330
  const workLoop = (deadline) => {
1283
1331
  const state = getState();
@@ -1285,6 +1333,8 @@ const workLoop = (deadline) => {
1285
1333
  while ((state.nextUnitOfWork || workQueue.length > 0) && !shouldYield) {
1286
1334
  if (!state.nextUnitOfWork && workQueue.length > 0) {
1287
1335
  const nextRoot = workQueue.shift();
1336
+ if (!nextRoot)
1337
+ continue;
1288
1338
  state.wipRoot = nextRoot;
1289
1339
  state.nextUnitOfWork = nextRoot;
1290
1340
  state.deletions = [];
@@ -1345,9 +1395,7 @@ const render = (element, container) => {
1345
1395
  const root = {
1346
1396
  dom: container,
1347
1397
  props: {
1348
- children: [
1349
- element,
1350
- ],
1398
+ children: [element],
1351
1399
  },
1352
1400
  alternate: state.currentRoot,
1353
1401
  isHydrating: false,
@@ -1360,7 +1408,7 @@ const SSR_ROOT_ATTR = 'data-ryunix-ssr-root';
1360
1408
  const nextValidSibling = (node) => {
1361
1409
  let next = node;
1362
1410
  while (next &&
1363
- ((next.nodeType === 3 && !next.nodeValue.trim()) ||
1411
+ ((next.nodeType === 3 && !next.nodeValue?.trim()) ||
1364
1412
  next.nodeType === 8 ||
1365
1413
  (next.nodeType === 1 &&
1366
1414
  next.hasAttribute('data-ryunix-ssr')))) {
@@ -1374,9 +1422,7 @@ const hydrate = (element, container) => {
1374
1422
  const root = {
1375
1423
  dom: container,
1376
1424
  props: {
1377
- children: [
1378
- element,
1379
- ],
1425
+ children: [element],
1380
1426
  },
1381
1427
  alternate: state.currentRoot,
1382
1428
  isHydrating: true,
@@ -1385,7 +1431,7 @@ const hydrate = (element, container) => {
1385
1431
  scheduleWork(root);
1386
1432
  return root;
1387
1433
  };
1388
- const init = (MainElement, root = '__ryunix', components = {}) => {
1434
+ const init = (MainElement, root = '__ryunix', _components = {}) => {
1389
1435
  const state = getState();
1390
1436
  const container = document.getElementById(root);
1391
1437
  state.containerRoot = container;
@@ -1482,6 +1528,82 @@ const validateHookContext = (hookName = 'A hook') => {
1482
1528
  }
1483
1529
  };
1484
1530
 
1531
+ const INTERNAL_META_KEYS = new Set([
1532
+ 'title',
1533
+ 'pageTitle',
1534
+ 'canonical',
1535
+ 'titleTemplate',
1536
+ 'titleDefault',
1537
+ 'lastmod',
1538
+ 'changefreq',
1539
+ 'priority',
1540
+ 'custom',
1541
+ 'icon',
1542
+ 'appleTouchIcon',
1543
+ ]);
1544
+ const isTitleConfig = (value) => is.object(value) &&
1545
+ value !== null &&
1546
+ ('default' in value || 'template' in value);
1547
+ const pickString = (value) => typeof value === 'string' && value.trim() ? value.trim() : undefined;
1548
+ function resolvePageMetadata(meta = {}, options = {}) {
1549
+ const template = pickString(options.title?.template) ||
1550
+ pickString(meta.titleTemplate) ||
1551
+ (isTitleConfig(meta.title) ? pickString(meta.title.template) : undefined);
1552
+ const defaultTitle = pickString(options.title?.prefix) ||
1553
+ pickString(meta.titleDefault) ||
1554
+ (isTitleConfig(meta.title) ? pickString(meta.title.default) : undefined) ||
1555
+ 'Ryunix App';
1556
+ const pageTitle = pickString(meta.pageTitle) ||
1557
+ (typeof meta.title === 'string' ? pickString(meta.title) : undefined);
1558
+ let title = defaultTitle;
1559
+ if (pageTitle) {
1560
+ title =
1561
+ template && template.includes('%s')
1562
+ ? template.replace('%s', pageTitle)
1563
+ : pageTitle;
1564
+ }
1565
+ const tags = {};
1566
+ for (const [key, value] of Object.entries(meta)) {
1567
+ if (INTERNAL_META_KEYS.has(key))
1568
+ continue;
1569
+ if (key === 'title' && isTitleConfig(value))
1570
+ continue;
1571
+ if (Array.isArray(value)) {
1572
+ const items = value
1573
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
1574
+ .filter(Boolean);
1575
+ if (items.length > 0)
1576
+ tags[key] = items;
1577
+ continue;
1578
+ }
1579
+ if (typeof value === 'string' && value.trim()) {
1580
+ tags[key] = value.trim();
1581
+ }
1582
+ }
1583
+ if (pickString(meta.canonical)) {
1584
+ tags.canonical = meta.canonical;
1585
+ }
1586
+ if (pickString(meta.icon)) {
1587
+ tags.icon = meta.icon;
1588
+ }
1589
+ if (pickString(meta.appleTouchIcon)) {
1590
+ tags.appleTouchIcon = meta.appleTouchIcon;
1591
+ }
1592
+ return { title, tags };
1593
+ }
1594
+ function mergeRouteMetadata(base = {}, next = {}) {
1595
+ const merged = { ...base, ...next };
1596
+ if (typeof next.title === 'string' && isTitleConfig(base.title)) {
1597
+ if (!pickString(merged.titleTemplate) && pickString(base.title.template)) {
1598
+ merged.titleTemplate = base.title.template;
1599
+ }
1600
+ if (!pickString(merged.titleDefault) && pickString(base.title.default)) {
1601
+ merged.titleDefault = base.title.default;
1602
+ }
1603
+ }
1604
+ return merged;
1605
+ }
1606
+
1485
1607
  const haveDepsChanged = (oldDeps, newDeps) => {
1486
1608
  if (!oldDeps || !newDeps)
1487
1609
  return true;
@@ -1521,6 +1643,8 @@ const useReducer = (reducer, initialState, init, defaultPriority = getCurrentPri
1521
1643
  validateHookContext();
1522
1644
  const { hookIndex } = state;
1523
1645
  const wipFiber = state.wipFiber;
1646
+ if (!wipFiber.hooks)
1647
+ wipFiber.hooks = [];
1524
1648
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1525
1649
  const hook = {
1526
1650
  hookID: hookIndex,
@@ -1560,8 +1684,7 @@ const useReducer = (reducer, initialState, init, defaultPriority = getCurrentPri
1560
1684
  };
1561
1685
  queueUpdate(() => scheduleWork$1(newRoot, priority));
1562
1686
  };
1563
- wipFiber.hooks[hookIndex] =
1564
- hook;
1687
+ wipFiber.hooks[hookIndex] = hook;
1565
1688
  state.hookIndex++;
1566
1689
  return [hook.state, dispatch];
1567
1690
  };
@@ -1582,6 +1705,8 @@ const useEffect = (callback, deps) => {
1582
1705
  }
1583
1706
  const { hookIndex } = state;
1584
1707
  const wipFiber = state.wipFiber;
1708
+ if (!wipFiber.hooks)
1709
+ wipFiber.hooks = [];
1585
1710
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1586
1711
  const hasChanged = haveDepsChanged(oldHook?.deps, deps);
1587
1712
  const hook = {
@@ -1591,8 +1716,7 @@ const useEffect = (callback, deps) => {
1591
1716
  effect: hasChanged ? callback : null,
1592
1717
  cancel: oldHook?.cancel,
1593
1718
  };
1594
- wipFiber.hooks[hookIndex] =
1595
- hook;
1719
+ wipFiber.hooks[hookIndex] = hook;
1596
1720
  state.hookIndex++;
1597
1721
  };
1598
1722
  const useRef = (initialValue) => {
@@ -1606,6 +1730,8 @@ const useRef = (initialValue) => {
1606
1730
  validateHookContext();
1607
1731
  const { hookIndex } = state;
1608
1732
  const wipFiber = state.wipFiber;
1733
+ if (!wipFiber.hooks)
1734
+ wipFiber.hooks = [];
1609
1735
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1610
1736
  const hook = {
1611
1737
  hookID: hookIndex,
@@ -1614,8 +1740,7 @@ const useRef = (initialValue) => {
1614
1740
  ? oldHook.value
1615
1741
  : { current: initialValue },
1616
1742
  };
1617
- wipFiber.hooks[hookIndex] =
1618
- hook;
1743
+ wipFiber.hooks[hookIndex] = hook;
1619
1744
  state.hookIndex++;
1620
1745
  return hook.value;
1621
1746
  };
@@ -1636,6 +1761,8 @@ const useMemo = (compute, deps) => {
1636
1761
  }
1637
1762
  const { hookIndex } = state;
1638
1763
  const wipFiber = state.wipFiber;
1764
+ if (!wipFiber.hooks)
1765
+ wipFiber.hooks = [];
1639
1766
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
1640
1767
  let value;
1641
1768
  if (oldHook && !haveDepsChanged(oldHook.deps, deps)) {
@@ -1658,8 +1785,7 @@ const useMemo = (compute, deps) => {
1658
1785
  value,
1659
1786
  deps,
1660
1787
  };
1661
- wipFiber.hooks[hookIndex] =
1662
- hook;
1788
+ wipFiber.hooks[hookIndex] = hook;
1663
1789
  state.hookIndex++;
1664
1790
  return value;
1665
1791
  };
@@ -1670,7 +1796,7 @@ const useCallback = (callback, deps) => {
1670
1796
  return useMemo(() => callback, deps);
1671
1797
  };
1672
1798
  const createContext = (contextId = RYUNIX_TYPES.RYUNIX_CONTEXT, defaultValue = {}) => {
1673
- const Provider = ({ value, children }) => {
1799
+ const Provider = ({ value, children, }) => {
1674
1800
  return createElement(RYUNIX_TYPES.RYUNIX_CONTEXT, { value, children, _contextId: contextId }, ...flattenArray([children]));
1675
1801
  };
1676
1802
  Provider._contextId = contextId;
@@ -1725,7 +1851,7 @@ const useHash = () => {
1725
1851
  const useMetadata = (tags = {}, options = {}) => {
1726
1852
  const state = getState();
1727
1853
  if (state.isServerRendering) {
1728
- state.ssrMetadata = { ...state.ssrMetadata, ...tags };
1854
+ state.ssrMetadata = mergeRouteMetadata((state.ssrMetadata || {}), tags);
1729
1855
  return;
1730
1856
  }
1731
1857
  useEffect(() => {
@@ -1756,6 +1882,8 @@ const useMetadata = (tags = {}, options = {}) => {
1756
1882
  Object.entries(tags).forEach(([key, value]) => {
1757
1883
  if (['title', 'pageTitle', 'canonical'].includes(key))
1758
1884
  return;
1885
+ if (value == null)
1886
+ return;
1759
1887
  const isProperty = key.startsWith('og:') || key.startsWith('twitter:');
1760
1888
  const selector = `meta[${isProperty ? 'property' : 'name'}='${key}']`;
1761
1889
  let meta = document.head.querySelector(selector);
@@ -1779,7 +1907,7 @@ const findRoute = (routes, path) => {
1779
1907
  const pathname = path.split('?')[0].split('#')[0];
1780
1908
  const notFoundRoute = routes.find((route) => route.NotFound);
1781
1909
  const notFound = notFoundRoute
1782
- ? { route: { component: notFoundRoute.NotFound }, params: {} }
1910
+ ? { route: { component: notFoundRoute.NotFound ?? null }, params: {} }
1783
1911
  : { route: { component: null }, params: {} };
1784
1912
  for (const route of routes) {
1785
1913
  if (route.subRoutes) {
@@ -1815,7 +1943,7 @@ const getSsrPathname = () => {
1815
1943
  }
1816
1944
  return '/';
1817
1945
  };
1818
- const RouterProvider = ({ routes, children }) => {
1946
+ const RouterProvider = ({ routes, children, }) => {
1819
1947
  if (typeof window === 'undefined') {
1820
1948
  const location = getSsrPathname();
1821
1949
  const currentRouteData = findRoute(routes, location);
@@ -1849,7 +1977,7 @@ const RouterProvider = ({ routes, children }) => {
1849
1977
  const currentRouteData = findRoute(routes, location);
1850
1978
  const query = useQuery();
1851
1979
  const contextValue = {
1852
- location,
1980
+ location: location,
1853
1981
  params: currentRouteData.params || {},
1854
1982
  query,
1855
1983
  navigate,
@@ -1871,10 +1999,13 @@ const Children = () => {
1871
1999
  const el = document.getElementById(id);
1872
2000
  if (el)
1873
2001
  el.scrollIntoView({ block: 'start', behavior: 'smooth' });
2002
+ return;
2003
+ }
2004
+ if (typeof window !== 'undefined') {
2005
+ window.scrollTo(0, 0);
1874
2006
  }
1875
- }, [hash]);
2007
+ }, [location, hash]);
1876
2008
  return createElement(route.component, {
1877
- key: location,
1878
2009
  params,
1879
2010
  query,
1880
2011
  hash,
@@ -1905,14 +2036,14 @@ const Link = ({ to, prefetch = true, ...props }) => {
1905
2036
  href: to,
1906
2037
  onClick: handleClick,
1907
2038
  onMouseEnter: handleMouseEnter,
1908
- className: props.className || props['ryunix-class'],
2039
+ className: (props.className || props['ryunix-class']),
1909
2040
  ...cleanedProps,
1910
2041
  }, props.children);
1911
2042
  };
1912
2043
  const NavLink = ({ to, exact = false, ...props }) => {
1913
2044
  const { location, navigate } = useRouter();
1914
2045
  const isActive = exact ? location === to : location.startsWith(to);
1915
- const resolveClass = (cls) => typeof cls === 'function' ? cls({ isActive }) : cls || '';
2046
+ const resolveClass = (cls) => (typeof cls === 'function' ? cls({ isActive }) : cls || '');
1916
2047
  const handleClick = (e) => {
1917
2048
  if (e.button !== 0 || e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
1918
2049
  return;
@@ -1921,7 +2052,7 @@ const NavLink = ({ to, exact = false, ...props }) => {
1921
2052
  navigate(to);
1922
2053
  };
1923
2054
  const classAttrName = props['ryunix-class'] ? 'ryunix-class' : 'className';
1924
- const classAttrValue = resolveClass(props['ryunix-class'] || props.className);
2055
+ const classAttrValue = resolveClass((props['ryunix-class'] || props.className));
1925
2056
  const { ['ryunix-class']: _omitRyunix, className: _omitClassName, ...cleanedProps } = props;
1926
2057
  return createElement('a', {
1927
2058
  href: to,
@@ -1973,7 +2104,7 @@ const usePersistentStore = (key, initialState = '') => {
1973
2104
  const item = window.localStorage.getItem(key);
1974
2105
  return item ? JSON.parse(item) : initialState;
1975
2106
  }
1976
- catch (error) {
2107
+ catch (_error) {
1977
2108
  return initialState;
1978
2109
  }
1979
2110
  });
@@ -2012,6 +2143,8 @@ const useLayoutEffect = (callback, deps) => {
2012
2143
  }
2013
2144
  const { hookIndex } = state;
2014
2145
  const wipFiber = state.wipFiber;
2146
+ if (!wipFiber.hooks)
2147
+ wipFiber.hooks = [];
2015
2148
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
2016
2149
  const hasChanged = haveDepsChanged(oldHook?.deps, deps);
2017
2150
  const hook = {
@@ -2022,8 +2155,7 @@ const useLayoutEffect = (callback, deps) => {
2022
2155
  cancel: oldHook?.cancel,
2023
2156
  isLayout: true,
2024
2157
  };
2025
- wipFiber.hooks[hookIndex] =
2026
- hook;
2158
+ wipFiber.hooks[hookIndex] = hook;
2027
2159
  state.hookIndex++;
2028
2160
  };
2029
2161
  let idCounter = 0;
@@ -2038,16 +2170,15 @@ const useId = () => {
2038
2170
  validateHookContext();
2039
2171
  const { hookIndex } = state;
2040
2172
  const wipFiber = state.wipFiber;
2173
+ if (!wipFiber.hooks)
2174
+ wipFiber.hooks = [];
2041
2175
  const oldHook = wipFiber.alternate?.hooks?.[hookIndex];
2042
2176
  const hook = {
2043
2177
  hookID: hookIndex,
2044
2178
  type: RYUNIX_TYPES.RYUNIX_REF,
2045
- value: oldHook
2046
- ? oldHook.value
2047
- : `:r${idCounter++}:`,
2179
+ value: oldHook ? oldHook.value : `:r${idCounter++}:`,
2048
2180
  };
2049
- wipFiber.hooks[hookIndex] =
2050
- hook;
2181
+ wipFiber.hooks[hookIndex] = hook;
2051
2182
  state.hookIndex++;
2052
2183
  return hook.value;
2053
2184
  };
@@ -2082,6 +2213,7 @@ const useThrottle = (value, interval = 300) => {
2082
2213
  return throttledValue;
2083
2214
  };
2084
2215
 
2216
+ const ASYNC_RENDER_ERROR = 'Async components require renderToStringAsync or renderToReadableStream, not renderToString';
2085
2217
  const escapeHtml = (unsafe) => {
2086
2218
  if (typeof unsafe !== 'string')
2087
2219
  return String(unsafe);
@@ -2116,6 +2248,83 @@ const VOID_ELEMENTS = new Set([
2116
2248
  'track',
2117
2249
  'wbr',
2118
2250
  ]);
2251
+ const normalizeChildren = (children) => {
2252
+ if (children == null)
2253
+ return [];
2254
+ return Array.isArray(children) ? children : [children];
2255
+ };
2256
+ const assertSyncRenderResult = (rendered) => {
2257
+ if (rendered != null &&
2258
+ typeof rendered === 'object' &&
2259
+ 'then' in rendered &&
2260
+ typeof rendered.then === 'function') {
2261
+ throw new Error(ASYNC_RENDER_ERROR);
2262
+ }
2263
+ return rendered;
2264
+ };
2265
+ const buildHostProps = (props, renderChild) => {
2266
+ let attributes = '';
2267
+ let htmlChildren = '';
2268
+ let innerHTML = null;
2269
+ Object.entries(props).forEach(([key, value]) => {
2270
+ if (key === 'children') {
2271
+ if (Array.isArray(value)) {
2272
+ htmlChildren = value
2273
+ .map((child) => renderChild(child))
2274
+ .join('');
2275
+ }
2276
+ else {
2277
+ htmlChildren = renderChild(value);
2278
+ }
2279
+ }
2280
+ else if (key === 'dangerouslySetInnerHTML') {
2281
+ const inner = value;
2282
+ if (inner?.__html) {
2283
+ innerHTML = inner.__html;
2284
+ }
2285
+ }
2286
+ else if (key === STRINGS.STYLE || key === OLD_STRINGS.STYLE) {
2287
+ const styleString = renderStyle(value);
2288
+ if (styleString) {
2289
+ attributes += ` style="${escapeHtml(styleString)}"`;
2290
+ }
2291
+ }
2292
+ else if (key === STRINGS.CLASS_NAME || key === OLD_STRINGS.CLASS_NAME) {
2293
+ if (value) {
2294
+ attributes += ` class="${escapeHtml(value)}"`;
2295
+ }
2296
+ }
2297
+ else if (!key.startsWith('on') &&
2298
+ key !== 'key' &&
2299
+ key !== 'ref' &&
2300
+ key !== '__source' &&
2301
+ key !== '__self') {
2302
+ if (typeof value === 'boolean') {
2303
+ if (value)
2304
+ attributes += ` ${key}=""`;
2305
+ }
2306
+ else if (value != null) {
2307
+ const attrName = toSvgAttrName(key);
2308
+ const validatedValue = validateUri(attrName, value);
2309
+ attributes += ` ${attrName}="${escapeHtml(validatedValue)}"`;
2310
+ }
2311
+ }
2312
+ });
2313
+ return { attributes, innerHTML, htmlChildren };
2314
+ };
2315
+ const formatHostOpenTag = (hostTag, attributes) => {
2316
+ if (VOID_ELEMENTS.has(hostTag)) {
2317
+ return `<${hostTag}${attributes} />`;
2318
+ }
2319
+ return `<${hostTag}${attributes}>`;
2320
+ };
2321
+ const beginSsrRender = () => {
2322
+ const state = getState();
2323
+ state.isServerRendering = true;
2324
+ state.ssrMetadata = {};
2325
+ state.ssrContexts = {};
2326
+ resetIdCounter();
2327
+ };
2119
2328
  const renderToStringImpl = (element) => {
2120
2329
  if (element == null || typeof element === 'boolean') {
2121
2330
  return '';
@@ -2128,20 +2337,18 @@ const renderToStringImpl = (element) => {
2128
2337
  }
2129
2338
  const vnode = element;
2130
2339
  if (vnode.type === RYUNIX_TYPES.TEXT_ELEMENT) {
2131
- return escapeHtml(vnode.props
2132
- .nodeValue);
2340
+ return escapeHtml(vnode.props.nodeValue);
2133
2341
  }
2134
2342
  if (vnode.type === RYUNIX_TYPES.RYUNIX_FRAGMENT) {
2135
- const children = vnode.props?.children || [];
2343
+ const children = normalizeChildren(vnode.props?.children);
2136
2344
  return children.map((child) => renderToStringImpl(child)).join('');
2137
2345
  }
2138
2346
  if (vnode.type === RYUNIX_TYPES.RYUNIX_CONTEXT) {
2139
2347
  const state = getState();
2140
2348
  state.ssrContexts = state.ssrContexts || {};
2141
- const ctxProps = vnode.props ||
2142
- {};
2349
+ const ctxProps = (vnode.props || {});
2143
2350
  const ctxId = ctxProps._contextId;
2144
- const prevCtx = state.ssrContexts[ctxId];
2351
+ const prevCtx = ctxId ? state.ssrContexts[ctxId] : undefined;
2145
2352
  if (ctxId) {
2146
2353
  state.ssrContexts[ctxId] = ctxProps.value;
2147
2354
  }
@@ -2161,61 +2368,17 @@ const renderToStringImpl = (element) => {
2161
2368
  if (typeof vnode.type === 'function') {
2162
2369
  const type = vnode.type;
2163
2370
  const props = vnode.props || {};
2164
- const renderedElement = type(props);
2371
+ const renderedElement = assertSyncRenderResult(type(props));
2165
2372
  return renderToStringImpl(renderedElement);
2166
2373
  }
2167
2374
  const type = String(vnode.type);
2168
2375
  const props = vnode.props || {};
2169
- let attributes = '';
2170
- let htmlChildren = '';
2171
- let innerHTML = null;
2172
- Object.entries(props).forEach(([key, value]) => {
2173
- if (key === 'children') {
2174
- if (Array.isArray(value)) {
2175
- htmlChildren = value.map((child) => renderToStringImpl(child)).join('');
2176
- }
2177
- else {
2178
- htmlChildren = renderToStringImpl(value);
2179
- }
2180
- }
2181
- else if (key === 'dangerouslySetInnerHTML') {
2182
- const inner = value;
2183
- if (inner?.__html) {
2184
- innerHTML = inner.__html;
2185
- }
2186
- }
2187
- else if (key === STRINGS.STYLE || key === OLD_STRINGS.STYLE) {
2188
- const styleString = renderStyle(value);
2189
- if (styleString) {
2190
- attributes += ` style="${escapeHtml(styleString)}"`;
2191
- }
2192
- }
2193
- else if (key === STRINGS.CLASS_NAME || key === OLD_STRINGS.CLASS_NAME) {
2194
- if (value) {
2195
- attributes += ` class="${escapeHtml(value)}"`;
2196
- }
2197
- }
2198
- else if (!key.startsWith('on') &&
2199
- key !== 'key' &&
2200
- key !== 'ref' &&
2201
- key !== '__source' &&
2202
- key !== '__self') {
2203
- if (typeof value === 'boolean') {
2204
- if (value)
2205
- attributes += ` ${key}=""`;
2206
- }
2207
- else if (value != null) {
2208
- let attrName = toSvgAttrName(key);
2209
- let validatedValue = validateUri(attrName, value);
2210
- attributes += ` ${attrName}="${escapeHtml(validatedValue)}"`;
2211
- }
2212
- }
2213
- });
2376
+ const { attributes, innerHTML, htmlChildren } = buildHostProps(props, renderToStringImpl);
2214
2377
  if (VOID_ELEMENTS.has(type)) {
2215
- return `<${type}${attributes} />`;
2378
+ return formatHostOpenTag(type, attributes);
2216
2379
  }
2217
2380
  const finalContent = innerHTML !== null ? innerHTML : htmlChildren;
2218
- return `<${type}${attributes}>${finalContent}</${type}>`;
2381
+ return `${formatHostOpenTag(type, attributes)}${finalContent}</${type}>`;
2219
2382
  };
2220
2383
  const RC_SCRIPT = `
2221
2384
  function $RC(id, templateId) {
@@ -2230,13 +2393,11 @@ function $RC(id, templateId) {
2230
2393
  .replace(/\s+/g, ' ')
2231
2394
  .trim();
2232
2395
  const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
2233
- if (element == null || typeof element === 'boolean') {
2234
- return;
2235
- }
2236
2396
  if (element instanceof Promise) {
2237
2397
  element = await element;
2238
- if (element == null || typeof element === 'boolean')
2239
- return;
2398
+ }
2399
+ if (element == null || typeof element === 'boolean') {
2400
+ return;
2240
2401
  }
2241
2402
  if (typeof element === 'string' || typeof element === 'number') {
2242
2403
  push(escapeHtml(element));
@@ -2250,12 +2411,11 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
2250
2411
  }
2251
2412
  const vnode = element;
2252
2413
  if (vnode.type === RYUNIX_TYPES.TEXT_ELEMENT) {
2253
- push(escapeHtml(vnode
2254
- .props.nodeValue));
2414
+ push(escapeHtml(vnode.props.nodeValue));
2255
2415
  return;
2256
2416
  }
2257
2417
  if (vnode.type === RYUNIX_TYPES.RYUNIX_FRAGMENT) {
2258
- const children = vnode.props?.children || [];
2418
+ const children = normalizeChildren(vnode.props?.children);
2259
2419
  for (const child of children) {
2260
2420
  await renderToStreamImpl(child, push, suspenseTasks);
2261
2421
  }
@@ -2264,10 +2424,9 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
2264
2424
  if (vnode.type === RYUNIX_TYPES.RYUNIX_CONTEXT) {
2265
2425
  const state = getState();
2266
2426
  state.ssrContexts = state.ssrContexts || {};
2267
- const ctxProps = vnode.props ||
2268
- {};
2427
+ const ctxProps = (vnode.props || {});
2269
2428
  const ctxId = ctxProps._contextId;
2270
- const prevCtx = state.ssrContexts[ctxId];
2429
+ const prevCtx = ctxId ? state.ssrContexts[ctxId] : undefined;
2271
2430
  if (ctxId) {
2272
2431
  state.ssrContexts[ctxId] = ctxProps.value;
2273
2432
  }
@@ -2289,11 +2448,9 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
2289
2448
  const isSuspenseBoundary = vnode.type === RYUNIX_TYPES.RYUNIX_SUSPENSE ||
2290
2449
  (typeof suspenseType === 'object' &&
2291
2450
  suspenseType != null &&
2292
- suspenseType.type ===
2293
- RYUNIX_TYPES.RYUNIX_SUSPENSE);
2451
+ suspenseType.type === RYUNIX_TYPES.RYUNIX_SUSPENSE);
2294
2452
  if (isSuspenseBoundary) {
2295
- const suspenseProps = vnode.props ||
2296
- {};
2453
+ const suspenseProps = (vnode.props || {});
2297
2454
  const { fallback, children } = suspenseProps;
2298
2455
  const id = `s-${Math.random().toString(36).slice(2, 9)}`;
2299
2456
  push(`<!--$?--><template id="B:${id}"></template><div id="S:${id}">`);
@@ -2315,14 +2472,14 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
2315
2472
  finally {
2316
2473
  state.isSuspenseBackground = wasBackground;
2317
2474
  }
2318
- })();
2475
+ });
2319
2476
  suspenseTasks.push(task);
2320
2477
  await renderToStreamImpl(fallback, push, suspenseTasks);
2321
2478
  push(`</div><!--$/-->`);
2322
2479
  return;
2323
2480
  }
2324
- let type = vnode.type;
2325
- let props = vnode.props || {};
2481
+ const type = vnode.type;
2482
+ const props = vnode.props || {};
2326
2483
  if (typeof type === 'function') {
2327
2484
  if (process.env.RYUNIX_DEBUG) {
2328
2485
  console.log('[SSR Debug] Rendering function:', type.name || 'anonymous');
@@ -2332,49 +2489,13 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
2332
2489
  return;
2333
2490
  }
2334
2491
  const hostTag = String(type);
2335
- let attributes = '';
2336
- let innerHTML = null;
2337
- let children = props.children || [];
2338
- Object.entries(props).forEach(([key, value]) => {
2339
- if (key === 'children') ;
2340
- else if (key === 'dangerouslySetInnerHTML') {
2341
- const inner = value;
2342
- if (inner?.__html) {
2343
- innerHTML = inner.__html;
2344
- }
2345
- }
2346
- else if (key === STRINGS.STYLE || key === OLD_STRINGS.STYLE) {
2347
- const styleString = renderStyle(value);
2348
- if (styleString) {
2349
- attributes += ` style="${escapeHtml(styleString)}"`;
2350
- }
2351
- }
2352
- else if (key === STRINGS.CLASS_NAME || key === OLD_STRINGS.CLASS_NAME) {
2353
- if (value) {
2354
- attributes += ` class="${escapeHtml(value)}"`;
2355
- }
2356
- }
2357
- else if (!key.startsWith('on') &&
2358
- key !== 'key' &&
2359
- key !== 'ref' &&
2360
- key !== '__source' &&
2361
- key !== '__self') {
2362
- if (typeof value === 'boolean') {
2363
- if (value)
2364
- attributes += ` ${key}=""`;
2365
- }
2366
- else if (value != null) {
2367
- const attrName = toSvgAttrName(key);
2368
- const validatedValue = validateUri(attrName, value);
2369
- attributes += ` ${attrName}="${escapeHtml(validatedValue)}"`;
2370
- }
2371
- }
2372
- });
2373
- push(`<${hostTag}${attributes}>`);
2492
+ const children = props.children || [];
2493
+ const { attributes, innerHTML } = buildHostProps(props, () => '');
2494
+ push(formatHostOpenTag(hostTag, attributes));
2374
2495
  if (innerHTML !== null) {
2375
2496
  push(innerHTML);
2376
2497
  }
2377
- else {
2498
+ else if (!VOID_ELEMENTS.has(hostTag)) {
2378
2499
  if (Array.isArray(children)) {
2379
2500
  for (const child of children) {
2380
2501
  await renderToStreamImpl(child, push, suspenseTasks);
@@ -2383,20 +2504,30 @@ const renderToStreamImpl = async (element, push, suspenseTasks = []) => {
2383
2504
  else {
2384
2505
  await renderToStreamImpl(children, push, suspenseTasks);
2385
2506
  }
2386
- }
2387
- if (!VOID_ELEMENTS.has(hostTag)) {
2388
2507
  push(`</${hostTag}>`);
2389
2508
  }
2390
2509
  };
2510
+ const handleSuspenseTaskResult = (res, push, nonceAttr) => {
2511
+ if (res.success) {
2512
+ push(`<template id="P:${res.id}" data-ryunix-ssr>${res.content}</template>`);
2513
+ push(`<script${nonceAttr} data-ryunix-ssr>$RC("S:${res.id}", "P:${res.id}")</script>`);
2514
+ return;
2515
+ }
2516
+ const message = res.error instanceof Error
2517
+ ? res.error.message
2518
+ : String(res.error ?? 'Unknown error');
2519
+ if (process.env.NODE_ENV !== 'production') {
2520
+ console.error('[Ryunix SSR] Suspense boundary failed:', res.error);
2521
+ }
2522
+ push(`<!-- Ryunix Suspense error: ${escapeHtml(message)} -->`);
2523
+ };
2391
2524
  const renderToReadableStream = (element, options = {}) => {
2392
2525
  const state = getState();
2393
2526
  const encoder = new TextEncoder();
2394
- resetIdCounter();
2395
2527
  return new ReadableStream({
2396
2528
  async start(controller) {
2397
2529
  const wasServerRendering = state.isServerRendering;
2398
- state.isServerRendering = true;
2399
- state.ssrMetadata = {};
2530
+ beginSsrRender();
2400
2531
  const push = (text) => controller.enqueue(encoder.encode(text));
2401
2532
  const suspenseTasks = [];
2402
2533
  try {
@@ -2405,11 +2536,10 @@ const renderToReadableStream = (element, options = {}) => {
2405
2536
  await renderToStreamImpl(element, push, suspenseTasks);
2406
2537
  while (suspenseTasks.length > 0) {
2407
2538
  const task = suspenseTasks.shift();
2408
- const res = await task;
2409
- if (res.success) {
2410
- push(`<template id="P:${res.id}" data-ryunix-ssr>${res.content}</template>`);
2411
- push(`<script${nonceAttr} data-ryunix-ssr>$RC("S:${res.id}", "P:${res.id}")</script>`);
2412
- }
2539
+ if (!task)
2540
+ continue;
2541
+ const res = await task();
2542
+ handleSuspenseTaskResult(res, push, nonceAttr);
2413
2543
  }
2414
2544
  controller.close();
2415
2545
  }
@@ -2422,12 +2552,10 @@ const renderToReadableStream = (element, options = {}) => {
2422
2552
  },
2423
2553
  });
2424
2554
  };
2425
- const renderToString = (element, options = {}) => {
2555
+ const renderToString = (element, _options = {}) => {
2426
2556
  const state = getState();
2427
2557
  const wasServerRendering = state.isServerRendering;
2428
- state.isServerRendering = true;
2429
- state.ssrMetadata = {};
2430
- resetIdCounter();
2558
+ beginSsrRender();
2431
2559
  try {
2432
2560
  return renderToStringImpl(element);
2433
2561
  }
@@ -2581,7 +2709,9 @@ const Suspense = ({ fallback, children, }) => {
2581
2709
  if (anyPending && !getState().isSuspenseBackground) {
2582
2710
  return fallback || null;
2583
2711
  }
2584
- return createElement(Fragment, { children });
2712
+ return createElement(Fragment, {
2713
+ children: children,
2714
+ });
2585
2715
  };
2586
2716
  Suspense.type = RYUNIX_TYPES.RYUNIX_SUSPENSE;
2587
2717
  function preload(importFn) {
@@ -2591,6 +2721,7 @@ function preload(importFn) {
2591
2721
  var index = /*#__PURE__*/Object.freeze({
2592
2722
  __proto__: null,
2593
2723
  Children: Children,
2724
+ INTERNAL_META_KEYS: INTERNAL_META_KEYS,
2594
2725
  Link: Link,
2595
2726
  NavLink: NavLink,
2596
2727
  RouterProvider: RouterProvider,
@@ -2600,8 +2731,10 @@ var index = /*#__PURE__*/Object.freeze({
2600
2731
  forwardRef: forwardRef,
2601
2732
  lazy: lazy,
2602
2733
  memo: memo,
2734
+ mergeRouteMetadata: mergeRouteMetadata,
2603
2735
  preload: preload,
2604
2736
  resetIdCounter: resetIdCounter,
2737
+ resolvePageMetadata: resolvePageMetadata,
2605
2738
  shallowEqual: shallowEqual,
2606
2739
  useCallback: useCallback,
2607
2740
  useDebounce: useDebounce,
@@ -2748,11 +2881,11 @@ function withProfiler(Component, name) {
2748
2881
  return Profiled;
2749
2882
  }
2750
2883
 
2751
- function ServerBoundary({ children, id }) {
2884
+ function ServerBoundary({ children, id, }) {
2752
2885
  return createElement('div', { 'data-ryunix-server': id, style: { display: 'contents' } }, children);
2753
2886
  }
2754
2887
  ServerBoundary.ryunix_type = 'RYUNIX_SERVER_BOUNDARY';
2755
- function HydrationBoundary({ children, id }) {
2888
+ function HydrationBoundary({ children, id, }) {
2756
2889
  return createElement('div', {
2757
2890
  'data-ryunix-hydrate-boundary': id ?? '',
2758
2891
  suppressHydrationWarning: true,
@@ -2787,7 +2920,7 @@ function createActionProxy(actionId) {
2787
2920
  body: JSON.stringify({ actionId, args }),
2788
2921
  });
2789
2922
  if (!response.ok) {
2790
- const errorData = await response.json().catch(() => ({}));
2923
+ const errorData = (await response.json().catch(() => ({})));
2791
2924
  throw new Error(errorData.error || 'Server Action failed');
2792
2925
  }
2793
2926
  return response.json();
@@ -2813,9 +2946,7 @@ function RyunixDevOverlay(propsOrError) {
2813
2946
  else if ('error' in rawError) {
2814
2947
  const nested = rawError.error;
2815
2948
  error =
2816
- nested && typeof nested === 'object'
2817
- ? nested
2818
- : null;
2949
+ nested && typeof nested === 'object' ? nested : null;
2819
2950
  }
2820
2951
  else {
2821
2952
  error = rawError;
@@ -3153,9 +3284,7 @@ function RyunixDevOverlay(propsOrError) {
3153
3284
  if (parts.length >= 2) {
3154
3285
  const lastPart = parts[parts.length - 1];
3155
3286
  const colonIdx = lastPart.indexOf(':');
3156
- const fileCandidate = colonIdx > 0
3157
- ? lastPart.slice(0, colonIdx)
3158
- : lastPart;
3287
+ const fileCandidate = colonIdx > 0 ? lastPart.slice(0, colonIdx) : lastPart;
3159
3288
  if (exts.some((ext) => fileCandidate.endsWith(ext))) {
3160
3289
  fnName = parts.slice(0, -1).join(' ');
3161
3290
  filePath = lastPart;
@@ -3442,9 +3571,243 @@ function Footer({ image, title, description, href = '/', imageAlt, className = '
3442
3571
  : null, bottomBar));
3443
3572
  }
3444
3573
 
3574
+ function getRyunixI18nConfig() {
3575
+ try {
3576
+ const value = ryunix.config.i18n;
3577
+ if (value && Array.isArray(value.locales) && value.locales.length > 0) {
3578
+ return value;
3579
+ }
3580
+ }
3581
+ catch {
3582
+ }
3583
+ return null;
3584
+ }
3585
+
3586
+ function defineMessages(messages) {
3587
+ return messages;
3588
+ }
3589
+ function resolveMessageKey(tree, key) {
3590
+ if (!tree)
3591
+ return undefined;
3592
+ const parts = key.split('.');
3593
+ let node = tree;
3594
+ for (const part of parts) {
3595
+ if (node == null || typeof node === 'string')
3596
+ return undefined;
3597
+ node = node[part];
3598
+ }
3599
+ return typeof node === 'string' ? node : undefined;
3600
+ }
3601
+ function formatMessage(template, params) {
3602
+ if (!params)
3603
+ return template;
3604
+ return Object.entries(params).reduce((value, [name, replacement]) => value.replace(new RegExp(`\\{${escapeRegExp$2(name)}\\}`, 'g'), String(replacement)), template);
3605
+ }
3606
+ function escapeRegExp$2(value) {
3607
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3608
+ }
3609
+
3610
+ function normalizeI18nConfig(config) {
3611
+ const locales = [...new Set(config.locales.filter(Boolean))];
3612
+ if (locales.length === 0) {
3613
+ throw new Error('[Ryunix i18n] At least one locale is required.');
3614
+ }
3615
+ const defaultLocale = locales.includes(config.defaultLocale)
3616
+ ? config.defaultLocale
3617
+ : locales[0];
3618
+ return { ...config, locales, defaultLocale };
3619
+ }
3620
+ function pickLocale(record, locale, fallback) {
3621
+ return record?.[locale] ?? record?.[fallback];
3622
+ }
3623
+ function getLocaleFromPath(pathname, locales) {
3624
+ if (typeof pathname !== 'string' || locales.length === 0)
3625
+ return null;
3626
+ const pattern = new RegExp(`^/(${locales.map(escapeRegExp$1).join('|')})(/|$)`);
3627
+ const match = pathname.match(pattern);
3628
+ return match ? match[1] : null;
3629
+ }
3630
+ function localePath(locale, path = '', locales) {
3631
+ const normalized = path.startsWith('/') ? path : path ? `/${path}` : '';
3632
+ if (!locales.includes(locale))
3633
+ return normalized || '/';
3634
+ if (!normalized || normalized === '/')
3635
+ return `/${locale}`;
3636
+ return `/${locale}${normalized}`;
3637
+ }
3638
+ function swapLocalePath(pathname, targetLocale, options) {
3639
+ const { locales } = options;
3640
+ if (!locales.includes(targetLocale))
3641
+ return pathname;
3642
+ const current = getLocaleFromPath(pathname, locales);
3643
+ if (current) {
3644
+ const rest = pathname.slice(current.length + 1) || '';
3645
+ return localePath(targetLocale, rest, locales);
3646
+ }
3647
+ if (pathname.startsWith('/')) {
3648
+ return localePath(targetLocale, pathname, locales);
3649
+ }
3650
+ return localePath(targetLocale, '', locales);
3651
+ }
3652
+ function escapeRegExp$1(value) {
3653
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3654
+ }
3655
+
3656
+ const DEFAULT_LOCALE_COOKIE_NAME = 'ryunix_locale';
3657
+ function resolveCookieOptions(cookie, cookieName, maxAgeSeconds) {
3658
+ const enabled = cookie !== false;
3659
+ const cookieConfig = typeof cookie === 'object' ? cookie : {};
3660
+ return {
3661
+ enabled,
3662
+ cookieName: cookieName ?? cookieConfig.name ?? DEFAULT_LOCALE_COOKIE_NAME,
3663
+ maxAgeSeconds: maxAgeSeconds ?? cookieConfig.maxAgeSeconds ?? 365 * 24 * 60 * 60,
3664
+ };
3665
+ }
3666
+ function normalizeOptions(options) {
3667
+ const base = normalizeI18nConfig({
3668
+ locales: options.locales,
3669
+ defaultLocale: options.defaultLocale,
3670
+ });
3671
+ const cookie = resolveCookieOptions(options.cookie, options.cookieName, options.maxAgeSeconds);
3672
+ const localeLabels = { ...options.localeLabels };
3673
+ for (const locale of base.locales) {
3674
+ if (!localeLabels[locale])
3675
+ localeLabels[locale] = locale.toUpperCase();
3676
+ }
3677
+ return {
3678
+ locales: base.locales,
3679
+ defaultLocale: base.defaultLocale,
3680
+ localeLabels,
3681
+ cookieName: cookie.cookieName,
3682
+ maxAgeSeconds: cookie.maxAgeSeconds,
3683
+ cookieEnabled: cookie.enabled,
3684
+ messages: options.messages ?? {},
3685
+ };
3686
+ }
3687
+ function createTranslate(messages, locale, defaultLocale) {
3688
+ return (key, params) => {
3689
+ const value = resolveMessageKey(messages[locale], key) ??
3690
+ resolveMessageKey(messages[defaultLocale], key) ??
3691
+ key;
3692
+ return formatMessage(value, params);
3693
+ };
3694
+ }
3695
+ function createI18n(options) {
3696
+ const config = normalizeOptions(options);
3697
+ const { Provider: I18nContextProvider, useContext } = createContext('ryunix.i18n', {
3698
+ locale: config.defaultLocale,
3699
+ defaultLocale: config.defaultLocale,
3700
+ locales: config.locales,
3701
+ localeLabels: config.localeLabels,
3702
+ t: (key) => key,
3703
+ });
3704
+ const isLocale = (value) => typeof value === 'string' && config.locales.includes(value);
3705
+ const getLocaleCookie = () => {
3706
+ if (!config.cookieEnabled || typeof document === 'undefined')
3707
+ return null;
3708
+ const pattern = new RegExp(`(?:^|;\\s*)${config.cookieName}=(${config.locales.map(escapeRegExp).join('|')})(?:;|$)`);
3709
+ const match = document.cookie.match(pattern);
3710
+ return match ? match[1] : null;
3711
+ };
3712
+ const setLocaleCookie = (locale) => {
3713
+ if (!config.cookieEnabled ||
3714
+ typeof document === 'undefined' ||
3715
+ !isLocale(locale))
3716
+ return;
3717
+ document.cookie = `${config.cookieName}=${locale}; path=/; max-age=${config.maxAgeSeconds}; SameSite=Lax`;
3718
+ };
3719
+ const resolveLocaleFromCookie = () => getLocaleCookie() || config.defaultLocale;
3720
+ const getLocaleRedirectScript = () => {
3721
+ if (!config.cookieEnabled)
3722
+ return '';
3723
+ const localesPattern = config.locales.map(escapeRegExp).join('|');
3724
+ 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){}})();`;
3725
+ };
3726
+ const Provider = ({ locale, children, }) => {
3727
+ const resolved = isLocale(locale) ? locale : config.defaultLocale;
3728
+ const value = {
3729
+ locale: resolved,
3730
+ defaultLocale: config.defaultLocale,
3731
+ locales: config.locales,
3732
+ localeLabels: config.localeLabels,
3733
+ t: createTranslate(config.messages, resolved, config.defaultLocale),
3734
+ };
3735
+ return createElement(I18nContextProvider, { value, children });
3736
+ };
3737
+ const useI18n = () => useContext();
3738
+ const useLocale = () => useI18n().locale;
3739
+ const useTranslations = () => useI18n().t;
3740
+ const generateStaticParams = () => config.locales.map((locale) => ({ locale }));
3741
+ const LocaleSwitcher = ({ className = '' }) => {
3742
+ const { location, navigate } = useRouter();
3743
+ const { locale: current, localeLabels, locales } = useI18n();
3744
+ const onSelect = (locale) => {
3745
+ if (locale === current)
3746
+ return;
3747
+ setLocaleCookie(locale);
3748
+ const next = swapLocalePath(location, locale, {
3749
+ locales,
3750
+ defaultLocale: config.defaultLocale,
3751
+ });
3752
+ navigate(next.startsWith(`/${locale}`) ? next : `/${locale}`);
3753
+ };
3754
+ return createElement('div', {
3755
+ className: `ryunix-locale-switcher ${className}`.trim(),
3756
+ role: 'group',
3757
+ 'aria-label': 'Language',
3758
+ children: locales.map((locale) => createElement('button', {
3759
+ key: locale,
3760
+ type: 'button',
3761
+ onClick: () => onSelect(locale),
3762
+ className: locale === current
3763
+ ? 'ryunix-locale-switcher__btn is-active'
3764
+ : 'ryunix-locale-switcher__btn',
3765
+ 'aria-current': locale === current ? 'true' : undefined,
3766
+ }, localeLabels[locale] ?? locale)),
3767
+ });
3768
+ };
3769
+ return {
3770
+ config,
3771
+ Provider,
3772
+ useI18n,
3773
+ useLocale,
3774
+ useTranslations,
3775
+ LocaleSwitcher,
3776
+ generateStaticParams,
3777
+ getLocaleFromPath: (pathname) => getLocaleFromPath(pathname, config.locales),
3778
+ localePath: (locale, path = '') => localePath(locale, path, config.locales),
3779
+ swapLocalePath: (pathname, targetLocale) => swapLocalePath(pathname, targetLocale, {
3780
+ locales: config.locales,
3781
+ defaultLocale: config.defaultLocale,
3782
+ }),
3783
+ pickLocale: (record, locale) => pickLocale(record, locale, config.defaultLocale),
3784
+ getLocaleCookie,
3785
+ setLocaleCookie,
3786
+ resolveLocaleFromCookie,
3787
+ getLocaleRedirectScript,
3788
+ };
3789
+ }
3790
+ function createAppI18n(messages, fallback) {
3791
+ const fromConfig = getRyunixI18nConfig() ?? fallback;
3792
+ if (!fromConfig) {
3793
+ 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.');
3794
+ }
3795
+ return createI18n({ ...fromConfig, messages });
3796
+ }
3797
+ function createI18nFromConfig(config, messages) {
3798
+ if (!config?.locales?.length) {
3799
+ throw new Error('[Ryunix i18n] Invalid i18n config: locales array is required.');
3800
+ }
3801
+ return createI18n({ ...config, messages });
3802
+ }
3803
+ function escapeRegExp(value) {
3804
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3805
+ }
3806
+
3445
3807
  var Ryunix = /*#__PURE__*/Object.freeze({
3446
3808
  __proto__: null,
3447
3809
  Children: Children,
3810
+ DEFAULT_LOCALE_COOKIE_NAME: DEFAULT_LOCALE_COOKIE_NAME,
3448
3811
  DEFAULT_THEME_COOKIE_NAME: DEFAULT_THEME_COOKIE_NAME,
3449
3812
  ErrorBoundary: ErrorBoundary,
3450
3813
  Footer: Footer,
@@ -3452,6 +3815,7 @@ var Ryunix = /*#__PURE__*/Object.freeze({
3452
3815
  Header: Header,
3453
3816
  Hooks: index,
3454
3817
  HydrationBoundary: HydrationBoundary,
3818
+ INTERNAL_META_KEYS: INTERNAL_META_KEYS,
3455
3819
  Link: Link,
3456
3820
  Main: Main,
3457
3821
  NavLink: NavLink,
@@ -3467,13 +3831,19 @@ var Ryunix = /*#__PURE__*/Object.freeze({
3467
3831
  batchUpdates: batchUpdates,
3468
3832
  cloneElement: cloneElement,
3469
3833
  createActionProxy: createActionProxy,
3834
+ createAppI18n: createAppI18n,
3470
3835
  createContext: createContext,
3471
3836
  createElement: createElement,
3837
+ createI18n: createI18n,
3838
+ createI18nFromConfig: createI18nFromConfig,
3472
3839
  createPortal: createPortal,
3473
3840
  createThemeController: createThemeController,
3474
3841
  deepEqual: deepEqual,
3842
+ defineMessages: defineMessages,
3475
3843
  escapeHtml: escapeHtml,
3476
3844
  forwardRef: forwardRef,
3845
+ getLocaleFromPath: getLocaleFromPath,
3846
+ getRyunixI18nConfig: getRyunixI18nConfig,
3477
3847
  getState: getState,
3478
3848
  getSystemColorScheme: getSystemColorScheme,
3479
3849
  getThemeCookie: getThemeCookie,
@@ -3481,12 +3851,16 @@ var Ryunix = /*#__PURE__*/Object.freeze({
3481
3851
  init: init,
3482
3852
  isValidElement: isValidElement,
3483
3853
  lazy: lazy,
3854
+ localePath: localePath,
3484
3855
  logHydrationBoundaryMismatch: logHydrationBoundaryMismatch,
3485
3856
  logHydrationBoundaryRecovery: logHydrationBoundaryRecovery,
3486
3857
  logHydrationFatal: logHydrationFatal,
3487
3858
  logHydrationInfo: logHydrationInfo,
3488
3859
  logHydrationRecoverable: logHydrationRecoverable,
3489
3860
  memo: memo,
3861
+ mergeRouteMetadata: mergeRouteMetadata,
3862
+ normalizeI18nConfig: normalizeI18nConfig,
3863
+ pickLocale: pickLocale,
3490
3864
  preload: preload,
3491
3865
  profiler: profiler,
3492
3866
  render: render,
@@ -3495,10 +3869,12 @@ var Ryunix = /*#__PURE__*/Object.freeze({
3495
3869
  renderToStringAsync: renderToStringAsync,
3496
3870
  resetIdCounter: resetIdCounter,
3497
3871
  resolveEffectiveTheme: resolveEffectiveTheme,
3872
+ resolvePageMetadata: resolvePageMetadata,
3498
3873
  resolveThemeFromCookie: resolveThemeFromCookie,
3499
3874
  safeRender: safeRender,
3500
3875
  setThemeCookie: setThemeCookie,
3501
3876
  shallowEqual: shallowEqual,
3877
+ swapLocalePath: swapLocalePath,
3502
3878
  themeController: themeController,
3503
3879
  themeInitScript: themeInitScript,
3504
3880
  useCallback: useCallback,
@@ -3590,7 +3966,7 @@ const defaultComponents = {
3590
3966
  const Image = ({ src, ...props }) => {
3591
3967
  return createElement('img', { ...props, src });
3592
3968
  };
3593
- const MDXContent = ({ children, components = {} }) => {
3969
+ const MDXContent = ({ children, components = {}, }) => {
3594
3970
  const mergedComponents = getMDXComponents(components);
3595
3971
  return createElement(MDXProvider, { value: mergedComponents }, createElement('div', null, children));
3596
3972
  };
@@ -3598,5 +3974,5 @@ const MDXContent = ({ children, components = {} }) => {
3598
3974
  if (typeof window !== 'undefined')
3599
3975
  window.Ryunix = Ryunix;
3600
3976
 
3601
- export { Children, DEFAULT_THEME_COOKIE_NAME, ErrorBoundary, Footer, Fragment, Header, index as Hooks, HydrationBoundary, Image, Link, MDXContent, MDXProvider, Main, NavLink, Priority, RouterProvider, RyunixDevOverlay, ServerBoundary, Suspense, THEME_PREFERENCES, ThemeInitScript, ThemeToggle, applyTheme, batchUpdates, cloneElement, createActionProxy, createContext, createElement, createPortal, createThemeController, deepEqual, Ryunix as default, defaultComponents, escapeHtml, forwardRef, getMDXComponents, getState, getSystemColorScheme, getThemeCookie, hydrate, init, isValidElement, lazy, logHydrationBoundaryMismatch, logHydrationBoundaryRecovery, logHydrationFatal, logHydrationInfo, logHydrationRecoverable, memo, preload, profiler, render, renderToReadableStream, renderToString, renderToStringAsync, resetIdCounter, resolveEffectiveTheme, resolveThemeFromCookie, ryxProps, safeRender, setThemeCookie, shallowEqual, themeController, themeInitScript, useCallback, useDebounce, useDeferredValue, useEffect, useHash, useId, useLayoutEffect, useMDXComponents, useMemo, useMetadata, usePathname, usePersistentStore, usePersistentStore as usePersitentStore, useProfiler, useQuery, useReducer, useRef, useRouter, useSearchParams, useStore, useStorePriority, useSwitch, useThrottle, useTransition, watchSystemTheme, withProfiler };
3977
+ export { Children, DEFAULT_LOCALE_COOKIE_NAME, DEFAULT_THEME_COOKIE_NAME, ErrorBoundary, Footer, Fragment, Header, index as Hooks, HydrationBoundary, INTERNAL_META_KEYS, Image, Link, MDXContent, MDXProvider, Main, NavLink, Priority, RouterProvider, RyunixDevOverlay, ServerBoundary, Suspense, THEME_PREFERENCES, ThemeInitScript, ThemeToggle, applyTheme, batchUpdates, cloneElement, createActionProxy, createAppI18n, createContext, createElement, createI18n, createI18nFromConfig, createPortal, createThemeController, deepEqual, Ryunix as default, defaultComponents, defineMessages, escapeHtml, forwardRef, getLocaleFromPath, getMDXComponents, getRyunixI18nConfig, getState, getSystemColorScheme, getThemeCookie, hydrate, init, isValidElement, lazy, localePath, logHydrationBoundaryMismatch, logHydrationBoundaryRecovery, logHydrationFatal, logHydrationInfo, logHydrationRecoverable, memo, mergeRouteMetadata, normalizeI18nConfig, pickLocale, preload, profiler, render, renderToReadableStream, renderToString, renderToStringAsync, resetIdCounter, resolveEffectiveTheme, resolvePageMetadata, resolveThemeFromCookie, ryxProps, safeRender, setThemeCookie, shallowEqual, swapLocalePath, themeController, themeInitScript, useCallback, useDebounce, useDeferredValue, useEffect, useHash, useId, useLayoutEffect, useMDXComponents, useMemo, useMetadata, usePathname, usePersistentStore, usePersistentStore as usePersitentStore, useProfiler, useQuery, useReducer, useRef, useRouter, useSearchParams, useStore, useStorePriority, useSwitch, useThrottle, useTransition, watchSystemTheme, withProfiler };
3602
3978
  //# sourceMappingURL=Ryunix.esm.js.map