hadars 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -190,7 +190,7 @@ function popContextValue(context, prev) {
190
190
  _g[MAP_KEY]?.set(context, prev);
191
191
  }
192
192
  var GLOBAL_KEY = "__slimReactRenderState";
193
- var EMPTY = { id: 0, overflow: "", bits: 0 };
193
+ var EMPTY = { id: 1, overflow: "" };
194
194
  function s() {
195
195
  const g = globalThis;
196
196
  if (!g[GLOBAL_KEY]) {
@@ -206,20 +206,27 @@ function resetRenderState() {
206
206
  function pushTreeContext(totalChildren, index) {
207
207
  const st = s();
208
208
  const saved = { ...st.currentTreeContext };
209
- const pendingBits = 32 - Math.clz32(totalChildren);
209
+ const baseIdWithLeadingBit = st.currentTreeContext.id;
210
+ const baseOverflow = st.currentTreeContext.overflow;
211
+ const baseLength = 31 - Math.clz32(baseIdWithLeadingBit);
212
+ let baseId = baseIdWithLeadingBit & ~(1 << baseLength);
210
213
  const slot = index + 1;
211
- const totalBits = st.currentTreeContext.bits + pendingBits;
212
- if (totalBits <= 30) {
214
+ const newBits = 32 - Math.clz32(totalChildren);
215
+ const length = newBits + baseLength;
216
+ if (30 < length) {
217
+ const numberOfOverflowBits = baseLength - baseLength % 5;
218
+ const overflowStr = (baseId & (1 << numberOfOverflowBits) - 1).toString(32);
219
+ baseId >>= numberOfOverflowBits;
220
+ const newBaseLength = baseLength - numberOfOverflowBits;
213
221
  st.currentTreeContext = {
214
- id: st.currentTreeContext.id << pendingBits | slot,
215
- overflow: st.currentTreeContext.overflow,
216
- bits: totalBits
222
+ id: 1 << newBits + newBaseLength | slot << newBaseLength | baseId,
223
+ overflow: overflowStr + baseOverflow
217
224
  };
218
225
  } else {
219
- let newOverflow = st.currentTreeContext.overflow;
220
- if (st.currentTreeContext.bits > 0)
221
- newOverflow += st.currentTreeContext.id.toString(32);
222
- st.currentTreeContext = { id: 1 << pendingBits | slot, overflow: newOverflow, bits: pendingBits };
226
+ st.currentTreeContext = {
227
+ id: 1 << length | slot << baseLength | baseId,
228
+ overflow: baseOverflow
229
+ };
223
230
  }
224
231
  return saved;
225
232
  }
@@ -235,6 +242,9 @@ function pushComponentScope() {
235
242
  function popComponentScope(saved) {
236
243
  s().localIdCounter = saved;
237
244
  }
245
+ function componentCalledUseId() {
246
+ return s().localIdCounter > 0;
247
+ }
238
248
  function snapshotContext() {
239
249
  const st = s();
240
250
  return { tree: { ...st.currentTreeContext }, localId: st.localIdCounter };
@@ -244,6 +254,121 @@ function restoreContext(snap) {
244
254
  st.currentTreeContext = { ...snap.tree };
245
255
  st.localIdCounter = snap.localId;
246
256
  }
257
+ function getTreeId() {
258
+ const { id, overflow } = s().currentTreeContext;
259
+ if (id === 1)
260
+ return overflow;
261
+ const stripped = (id & ~(1 << 31 - Math.clz32(id))).toString(32);
262
+ return stripped + overflow;
263
+ }
264
+ function makeId() {
265
+ const st = s();
266
+ const treeId = getTreeId();
267
+ const n = st.localIdCounter++;
268
+ let id = "\xAB" + st.idPrefix + "R" + treeId;
269
+ if (n > 0)
270
+ id += "H" + n.toString(32);
271
+ return id + "\xBB";
272
+ }
273
+
274
+ // src/slim-react/hooks.ts
275
+ function useState(initialState) {
276
+ const value = typeof initialState === "function" ? initialState() : initialState;
277
+ return [value, () => {
278
+ }];
279
+ }
280
+ function useReducer(_reducer, initialState) {
281
+ return [initialState, () => {
282
+ }];
283
+ }
284
+ function useEffect(_effect, _deps) {
285
+ }
286
+ function useLayoutEffect(_effect, _deps) {
287
+ }
288
+ function useInsertionEffect(_effect, _deps) {
289
+ }
290
+ function useRef(initialValue) {
291
+ return { current: initialValue };
292
+ }
293
+ function useMemo(factory, _deps) {
294
+ return factory();
295
+ }
296
+ function useCallback(callback, _deps) {
297
+ return callback;
298
+ }
299
+ function useDebugValue(_value, _format) {
300
+ }
301
+ function useImperativeHandle(_ref, _createHandle, _deps) {
302
+ }
303
+ function useSyncExternalStore(_subscribe, getSnapshot, getServerSnapshot) {
304
+ return (getServerSnapshot || getSnapshot)();
305
+ }
306
+ function useTransition() {
307
+ return [false, (cb) => cb()];
308
+ }
309
+ function useDeferredValue(value) {
310
+ return value;
311
+ }
312
+ function useOptimistic(passthrough) {
313
+ return [passthrough, () => {
314
+ }];
315
+ }
316
+ function useActionState(_action, initialState, _permalink) {
317
+ return [initialState, () => {
318
+ }, false];
319
+ }
320
+ function use(usable) {
321
+ if (typeof usable === "object" && usable !== null && ("_currentValue" in usable || "_defaultValue" in usable)) {
322
+ return getContextValue(usable);
323
+ }
324
+ const promise = usable;
325
+ if (promise.status === "fulfilled")
326
+ return promise.value;
327
+ if (promise.status === "rejected")
328
+ throw promise.reason;
329
+ throw promise;
330
+ }
331
+
332
+ // src/slim-react/dispatcher.ts
333
+ import ReactPkg from "react";
334
+ var _internals = ReactPkg.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
335
+ var slimDispatcher = {
336
+ useId: makeId,
337
+ readContext: (ctx) => getContextValue(ctx),
338
+ useContext: (ctx) => getContextValue(ctx),
339
+ useState,
340
+ useReducer,
341
+ useEffect,
342
+ useLayoutEffect,
343
+ useInsertionEffect,
344
+ useRef,
345
+ useMemo,
346
+ useCallback,
347
+ useDebugValue,
348
+ useImperativeHandle,
349
+ useSyncExternalStore,
350
+ useTransition,
351
+ useDeferredValue,
352
+ useOptimistic,
353
+ useActionState,
354
+ use,
355
+ // React internals that compiled output may call
356
+ useMemoCache: (size) => new Array(size).fill(void 0),
357
+ useCacheRefresh: () => () => {
358
+ },
359
+ useHostTransitionStatus: () => false
360
+ };
361
+ function installDispatcher() {
362
+ if (!_internals)
363
+ return null;
364
+ const prev = _internals.H;
365
+ _internals.H = slimDispatcher;
366
+ return prev;
367
+ }
368
+ function restoreDispatcher(prev) {
369
+ if (_internals)
370
+ _internals.H = prev;
371
+ }
247
372
 
248
373
  // src/slim-react/render.ts
249
374
  var VOID_ELEMENTS = /* @__PURE__ */ new Set([
@@ -425,7 +550,11 @@ function renderAttributes(props, isSvg) {
425
550
  continue;
426
551
  }
427
552
  if (value === true) {
428
- attrs += ` ${attrName}=""`;
553
+ if (attrName.startsWith("aria-") || attrName.startsWith("data-")) {
554
+ attrs += ` ${attrName}="true"`;
555
+ } else {
556
+ attrs += ` ${attrName}=""`;
557
+ }
429
558
  continue;
430
559
  }
431
560
  if (key === "style" && typeof value === "object") {
@@ -644,6 +773,7 @@ function renderComponent(type, props, writer, isSvg) {
644
773
  return;
645
774
  }
646
775
  let result;
776
+ const prevDispatcher = installDispatcher();
647
777
  try {
648
778
  if (type.prototype && typeof type.prototype.render === "function") {
649
779
  const instance = new type(props);
@@ -657,12 +787,20 @@ function renderComponent(type, props, writer, isSvg) {
657
787
  result = type(props);
658
788
  }
659
789
  } catch (e) {
790
+ restoreDispatcher(prevDispatcher);
660
791
  popComponentScope(savedScope);
661
792
  if (isProvider)
662
793
  popContextValue(ctx, prevCtxValue);
663
794
  throw e;
664
795
  }
796
+ restoreDispatcher(prevDispatcher);
797
+ let savedIdTree;
798
+ if (!(result instanceof Promise) && componentCalledUseId()) {
799
+ savedIdTree = pushTreeContext(1, 0);
800
+ }
665
801
  const finish = () => {
802
+ if (savedIdTree !== void 0)
803
+ popTreeContext(savedIdTree);
666
804
  popComponentScope(savedScope);
667
805
  if (isProvider)
668
806
  popContextValue(ctx, prevCtxValue);
@@ -671,22 +809,33 @@ function renderComponent(type, props, writer, isSvg) {
671
809
  const m = captureMap();
672
810
  return result.then((resolved) => {
673
811
  swapContextMap(m);
812
+ let asyncSavedIdTree;
813
+ if (componentCalledUseId()) {
814
+ asyncSavedIdTree = pushTreeContext(1, 0);
815
+ }
816
+ const asyncFinish = () => {
817
+ if (asyncSavedIdTree !== void 0)
818
+ popTreeContext(asyncSavedIdTree);
819
+ popComponentScope(savedScope);
820
+ if (isProvider)
821
+ popContextValue(ctx, prevCtxValue);
822
+ };
674
823
  const r2 = renderNode(resolved, writer, isSvg);
675
824
  if (r2 && typeof r2.then === "function") {
676
825
  const m2 = captureMap();
677
826
  return r2.then(
678
827
  () => {
679
828
  swapContextMap(m2);
680
- finish();
829
+ asyncFinish();
681
830
  },
682
831
  (e) => {
683
832
  swapContextMap(m2);
684
- finish();
833
+ asyncFinish();
685
834
  throw e;
686
835
  }
687
836
  );
688
837
  }
689
- finish();
838
+ asyncFinish();
690
839
  }, (e) => {
691
840
  swapContextMap(m);
692
841
  finish();
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/slim-react/index.ts
@@ -130,7 +140,7 @@ function popContextValue(context, prev) {
130
140
  _g[MAP_KEY]?.set(context, prev);
131
141
  }
132
142
  var GLOBAL_KEY = "__slimReactRenderState";
133
- var EMPTY = { id: 0, overflow: "", bits: 0 };
143
+ var EMPTY = { id: 1, overflow: "" };
134
144
  function s() {
135
145
  const g = globalThis;
136
146
  if (!g[GLOBAL_KEY]) {
@@ -146,20 +156,27 @@ function resetRenderState() {
146
156
  function pushTreeContext(totalChildren, index) {
147
157
  const st = s();
148
158
  const saved = { ...st.currentTreeContext };
149
- const pendingBits = 32 - Math.clz32(totalChildren);
159
+ const baseIdWithLeadingBit = st.currentTreeContext.id;
160
+ const baseOverflow = st.currentTreeContext.overflow;
161
+ const baseLength = 31 - Math.clz32(baseIdWithLeadingBit);
162
+ let baseId = baseIdWithLeadingBit & ~(1 << baseLength);
150
163
  const slot = index + 1;
151
- const totalBits = st.currentTreeContext.bits + pendingBits;
152
- if (totalBits <= 30) {
164
+ const newBits = 32 - Math.clz32(totalChildren);
165
+ const length = newBits + baseLength;
166
+ if (30 < length) {
167
+ const numberOfOverflowBits = baseLength - baseLength % 5;
168
+ const overflowStr = (baseId & (1 << numberOfOverflowBits) - 1).toString(32);
169
+ baseId >>= numberOfOverflowBits;
170
+ const newBaseLength = baseLength - numberOfOverflowBits;
153
171
  st.currentTreeContext = {
154
- id: st.currentTreeContext.id << pendingBits | slot,
155
- overflow: st.currentTreeContext.overflow,
156
- bits: totalBits
172
+ id: 1 << newBits + newBaseLength | slot << newBaseLength | baseId,
173
+ overflow: overflowStr + baseOverflow
157
174
  };
158
175
  } else {
159
- let newOverflow = st.currentTreeContext.overflow;
160
- if (st.currentTreeContext.bits > 0)
161
- newOverflow += st.currentTreeContext.id.toString(32);
162
- st.currentTreeContext = { id: 1 << pendingBits | slot, overflow: newOverflow, bits: pendingBits };
176
+ st.currentTreeContext = {
177
+ id: 1 << length | slot << baseLength | baseId,
178
+ overflow: baseOverflow
179
+ };
163
180
  }
164
181
  return saved;
165
182
  }
@@ -175,6 +192,9 @@ function pushComponentScope() {
175
192
  function popComponentScope(saved) {
176
193
  s().localIdCounter = saved;
177
194
  }
195
+ function componentCalledUseId() {
196
+ return s().localIdCounter > 0;
197
+ }
178
198
  function snapshotContext() {
179
199
  const st = s();
180
200
  return { tree: { ...st.currentTreeContext }, localId: st.localIdCounter };
@@ -185,20 +205,20 @@ function restoreContext(snap) {
185
205
  st.localIdCounter = snap.localId;
186
206
  }
187
207
  function getTreeId() {
188
- const { id, overflow, bits } = s().currentTreeContext;
189
- return bits > 0 ? overflow + id.toString(32) : overflow;
208
+ const { id, overflow } = s().currentTreeContext;
209
+ if (id === 1)
210
+ return overflow;
211
+ const stripped = (id & ~(1 << 31 - Math.clz32(id))).toString(32);
212
+ return stripped + overflow;
190
213
  }
191
214
  function makeId() {
192
215
  const st = s();
193
216
  const treeId = getTreeId();
194
217
  const n = st.localIdCounter++;
195
- let id = ":" + st.idPrefix + "R";
196
- if (treeId.length > 0)
197
- id += treeId;
198
- id += ":";
218
+ let id = "\xAB" + st.idPrefix + "R" + treeId;
199
219
  if (n > 0)
200
- id += "H" + n.toString(32) + ":";
201
- return id;
220
+ id += "H" + n.toString(32);
221
+ return id + "\xBB";
202
222
  }
203
223
 
204
224
  // src/slim-react/hooks.ts
@@ -291,6 +311,47 @@ function createContext(defaultValue) {
291
311
  return context;
292
312
  }
293
313
 
314
+ // src/slim-react/dispatcher.ts
315
+ var import_react = __toESM(require("react"), 1);
316
+ var _internals = import_react.default.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
317
+ var slimDispatcher = {
318
+ useId: makeId,
319
+ readContext: (ctx) => getContextValue(ctx),
320
+ useContext: (ctx) => getContextValue(ctx),
321
+ useState,
322
+ useReducer,
323
+ useEffect,
324
+ useLayoutEffect,
325
+ useInsertionEffect,
326
+ useRef,
327
+ useMemo,
328
+ useCallback,
329
+ useDebugValue,
330
+ useImperativeHandle,
331
+ useSyncExternalStore,
332
+ useTransition,
333
+ useDeferredValue,
334
+ useOptimistic,
335
+ useActionState,
336
+ use,
337
+ // React internals that compiled output may call
338
+ useMemoCache: (size) => new Array(size).fill(void 0),
339
+ useCacheRefresh: () => () => {
340
+ },
341
+ useHostTransitionStatus: () => false
342
+ };
343
+ function installDispatcher() {
344
+ if (!_internals)
345
+ return null;
346
+ const prev = _internals.H;
347
+ _internals.H = slimDispatcher;
348
+ return prev;
349
+ }
350
+ function restoreDispatcher(prev) {
351
+ if (_internals)
352
+ _internals.H = prev;
353
+ }
354
+
294
355
  // src/slim-react/render.ts
295
356
  var VOID_ELEMENTS = /* @__PURE__ */ new Set([
296
357
  "area",
@@ -471,7 +532,11 @@ function renderAttributes(props, isSvg) {
471
532
  continue;
472
533
  }
473
534
  if (value === true) {
474
- attrs += ` ${attrName}=""`;
535
+ if (attrName.startsWith("aria-") || attrName.startsWith("data-")) {
536
+ attrs += ` ${attrName}="true"`;
537
+ } else {
538
+ attrs += ` ${attrName}=""`;
539
+ }
475
540
  continue;
476
541
  }
477
542
  if (key === "style" && typeof value === "object") {
@@ -690,6 +755,7 @@ function renderComponent(type, props, writer, isSvg) {
690
755
  return;
691
756
  }
692
757
  let result;
758
+ const prevDispatcher = installDispatcher();
693
759
  try {
694
760
  if (type.prototype && typeof type.prototype.render === "function") {
695
761
  const instance = new type(props);
@@ -703,12 +769,20 @@ function renderComponent(type, props, writer, isSvg) {
703
769
  result = type(props);
704
770
  }
705
771
  } catch (e) {
772
+ restoreDispatcher(prevDispatcher);
706
773
  popComponentScope(savedScope);
707
774
  if (isProvider)
708
775
  popContextValue(ctx, prevCtxValue);
709
776
  throw e;
710
777
  }
778
+ restoreDispatcher(prevDispatcher);
779
+ let savedIdTree;
780
+ if (!(result instanceof Promise) && componentCalledUseId()) {
781
+ savedIdTree = pushTreeContext(1, 0);
782
+ }
711
783
  const finish = () => {
784
+ if (savedIdTree !== void 0)
785
+ popTreeContext(savedIdTree);
712
786
  popComponentScope(savedScope);
713
787
  if (isProvider)
714
788
  popContextValue(ctx, prevCtxValue);
@@ -717,22 +791,33 @@ function renderComponent(type, props, writer, isSvg) {
717
791
  const m = captureMap();
718
792
  return result.then((resolved) => {
719
793
  swapContextMap(m);
794
+ let asyncSavedIdTree;
795
+ if (componentCalledUseId()) {
796
+ asyncSavedIdTree = pushTreeContext(1, 0);
797
+ }
798
+ const asyncFinish = () => {
799
+ if (asyncSavedIdTree !== void 0)
800
+ popTreeContext(asyncSavedIdTree);
801
+ popComponentScope(savedScope);
802
+ if (isProvider)
803
+ popContextValue(ctx, prevCtxValue);
804
+ };
720
805
  const r2 = renderNode(resolved, writer, isSvg);
721
806
  if (r2 && typeof r2.then === "function") {
722
807
  const m2 = captureMap();
723
808
  return r2.then(
724
809
  () => {
725
810
  swapContextMap(m2);
726
- finish();
811
+ asyncFinish();
727
812
  },
728
813
  (e) => {
729
814
  swapContextMap(m2);
730
- finish();
815
+ asyncFinish();
731
816
  throw e;
732
817
  }
733
818
  );
734
819
  }
735
- finish();
820
+ asyncFinish();
736
821
  }, (e) => {
737
822
  swapContextMap(m);
738
823
  finish();
@@ -39,7 +39,7 @@ function popContextValue(context, prev) {
39
39
  _g[MAP_KEY]?.set(context, prev);
40
40
  }
41
41
  var GLOBAL_KEY = "__slimReactRenderState";
42
- var EMPTY = { id: 0, overflow: "", bits: 0 };
42
+ var EMPTY = { id: 1, overflow: "" };
43
43
  function s() {
44
44
  const g = globalThis;
45
45
  if (!g[GLOBAL_KEY]) {
@@ -55,20 +55,27 @@ function resetRenderState() {
55
55
  function pushTreeContext(totalChildren, index) {
56
56
  const st = s();
57
57
  const saved = { ...st.currentTreeContext };
58
- const pendingBits = 32 - Math.clz32(totalChildren);
58
+ const baseIdWithLeadingBit = st.currentTreeContext.id;
59
+ const baseOverflow = st.currentTreeContext.overflow;
60
+ const baseLength = 31 - Math.clz32(baseIdWithLeadingBit);
61
+ let baseId = baseIdWithLeadingBit & ~(1 << baseLength);
59
62
  const slot = index + 1;
60
- const totalBits = st.currentTreeContext.bits + pendingBits;
61
- if (totalBits <= 30) {
63
+ const newBits = 32 - Math.clz32(totalChildren);
64
+ const length = newBits + baseLength;
65
+ if (30 < length) {
66
+ const numberOfOverflowBits = baseLength - baseLength % 5;
67
+ const overflowStr = (baseId & (1 << numberOfOverflowBits) - 1).toString(32);
68
+ baseId >>= numberOfOverflowBits;
69
+ const newBaseLength = baseLength - numberOfOverflowBits;
62
70
  st.currentTreeContext = {
63
- id: st.currentTreeContext.id << pendingBits | slot,
64
- overflow: st.currentTreeContext.overflow,
65
- bits: totalBits
71
+ id: 1 << newBits + newBaseLength | slot << newBaseLength | baseId,
72
+ overflow: overflowStr + baseOverflow
66
73
  };
67
74
  } else {
68
- let newOverflow = st.currentTreeContext.overflow;
69
- if (st.currentTreeContext.bits > 0)
70
- newOverflow += st.currentTreeContext.id.toString(32);
71
- st.currentTreeContext = { id: 1 << pendingBits | slot, overflow: newOverflow, bits: pendingBits };
75
+ st.currentTreeContext = {
76
+ id: 1 << length | slot << baseLength | baseId,
77
+ overflow: baseOverflow
78
+ };
72
79
  }
73
80
  return saved;
74
81
  }
@@ -84,6 +91,9 @@ function pushComponentScope() {
84
91
  function popComponentScope(saved) {
85
92
  s().localIdCounter = saved;
86
93
  }
94
+ function componentCalledUseId() {
95
+ return s().localIdCounter > 0;
96
+ }
87
97
  function snapshotContext() {
88
98
  const st = s();
89
99
  return { tree: { ...st.currentTreeContext }, localId: st.localIdCounter };
@@ -94,20 +104,20 @@ function restoreContext(snap) {
94
104
  st.localIdCounter = snap.localId;
95
105
  }
96
106
  function getTreeId() {
97
- const { id, overflow, bits } = s().currentTreeContext;
98
- return bits > 0 ? overflow + id.toString(32) : overflow;
107
+ const { id, overflow } = s().currentTreeContext;
108
+ if (id === 1)
109
+ return overflow;
110
+ const stripped = (id & ~(1 << 31 - Math.clz32(id))).toString(32);
111
+ return stripped + overflow;
99
112
  }
100
113
  function makeId() {
101
114
  const st = s();
102
115
  const treeId = getTreeId();
103
116
  const n = st.localIdCounter++;
104
- let id = ":" + st.idPrefix + "R";
105
- if (treeId.length > 0)
106
- id += treeId;
107
- id += ":";
117
+ let id = "\xAB" + st.idPrefix + "R" + treeId;
108
118
  if (n > 0)
109
- id += "H" + n.toString(32) + ":";
110
- return id;
119
+ id += "H" + n.toString(32);
120
+ return id + "\xBB";
111
121
  }
112
122
 
113
123
  // src/slim-react/hooks.ts
@@ -200,6 +210,47 @@ function createContext(defaultValue) {
200
210
  return context;
201
211
  }
202
212
 
213
+ // src/slim-react/dispatcher.ts
214
+ import ReactPkg from "react";
215
+ var _internals = ReactPkg.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
216
+ var slimDispatcher = {
217
+ useId: makeId,
218
+ readContext: (ctx) => getContextValue(ctx),
219
+ useContext: (ctx) => getContextValue(ctx),
220
+ useState,
221
+ useReducer,
222
+ useEffect,
223
+ useLayoutEffect,
224
+ useInsertionEffect,
225
+ useRef,
226
+ useMemo,
227
+ useCallback,
228
+ useDebugValue,
229
+ useImperativeHandle,
230
+ useSyncExternalStore,
231
+ useTransition,
232
+ useDeferredValue,
233
+ useOptimistic,
234
+ useActionState,
235
+ use,
236
+ // React internals that compiled output may call
237
+ useMemoCache: (size) => new Array(size).fill(void 0),
238
+ useCacheRefresh: () => () => {
239
+ },
240
+ useHostTransitionStatus: () => false
241
+ };
242
+ function installDispatcher() {
243
+ if (!_internals)
244
+ return null;
245
+ const prev = _internals.H;
246
+ _internals.H = slimDispatcher;
247
+ return prev;
248
+ }
249
+ function restoreDispatcher(prev) {
250
+ if (_internals)
251
+ _internals.H = prev;
252
+ }
253
+
203
254
  // src/slim-react/render.ts
204
255
  var VOID_ELEMENTS = /* @__PURE__ */ new Set([
205
256
  "area",
@@ -380,7 +431,11 @@ function renderAttributes(props, isSvg) {
380
431
  continue;
381
432
  }
382
433
  if (value === true) {
383
- attrs += ` ${attrName}=""`;
434
+ if (attrName.startsWith("aria-") || attrName.startsWith("data-")) {
435
+ attrs += ` ${attrName}="true"`;
436
+ } else {
437
+ attrs += ` ${attrName}=""`;
438
+ }
384
439
  continue;
385
440
  }
386
441
  if (key === "style" && typeof value === "object") {
@@ -599,6 +654,7 @@ function renderComponent(type, props, writer, isSvg) {
599
654
  return;
600
655
  }
601
656
  let result;
657
+ const prevDispatcher = installDispatcher();
602
658
  try {
603
659
  if (type.prototype && typeof type.prototype.render === "function") {
604
660
  const instance = new type(props);
@@ -612,12 +668,20 @@ function renderComponent(type, props, writer, isSvg) {
612
668
  result = type(props);
613
669
  }
614
670
  } catch (e) {
671
+ restoreDispatcher(prevDispatcher);
615
672
  popComponentScope(savedScope);
616
673
  if (isProvider)
617
674
  popContextValue(ctx, prevCtxValue);
618
675
  throw e;
619
676
  }
677
+ restoreDispatcher(prevDispatcher);
678
+ let savedIdTree;
679
+ if (!(result instanceof Promise) && componentCalledUseId()) {
680
+ savedIdTree = pushTreeContext(1, 0);
681
+ }
620
682
  const finish = () => {
683
+ if (savedIdTree !== void 0)
684
+ popTreeContext(savedIdTree);
621
685
  popComponentScope(savedScope);
622
686
  if (isProvider)
623
687
  popContextValue(ctx, prevCtxValue);
@@ -626,22 +690,33 @@ function renderComponent(type, props, writer, isSvg) {
626
690
  const m = captureMap();
627
691
  return result.then((resolved) => {
628
692
  swapContextMap(m);
693
+ let asyncSavedIdTree;
694
+ if (componentCalledUseId()) {
695
+ asyncSavedIdTree = pushTreeContext(1, 0);
696
+ }
697
+ const asyncFinish = () => {
698
+ if (asyncSavedIdTree !== void 0)
699
+ popTreeContext(asyncSavedIdTree);
700
+ popComponentScope(savedScope);
701
+ if (isProvider)
702
+ popContextValue(ctx, prevCtxValue);
703
+ };
629
704
  const r2 = renderNode(resolved, writer, isSvg);
630
705
  if (r2 && typeof r2.then === "function") {
631
706
  const m2 = captureMap();
632
707
  return r2.then(
633
708
  () => {
634
709
  swapContextMap(m2);
635
- finish();
710
+ asyncFinish();
636
711
  },
637
712
  (e) => {
638
713
  swapContextMap(m2);
639
- finish();
714
+ asyncFinish();
640
715
  throw e;
641
716
  }
642
717
  );
643
718
  }
644
- finish();
719
+ asyncFinish();
645
720
  }, (e) => {
646
721
  swapContextMap(m);
647
722
  finish();
@@ -97,7 +97,7 @@ function popContextValue(context, prev) {
97
97
  _g[MAP_KEY]?.set(context, prev);
98
98
  }
99
99
  var GLOBAL_KEY = "__slimReactRenderState";
100
- var EMPTY = { id: 0, overflow: "", bits: 0 };
100
+ var EMPTY = { id: 1, overflow: "" };
101
101
  function s() {
102
102
  const g = globalThis;
103
103
  if (!g[GLOBAL_KEY]) {
@@ -113,20 +113,27 @@ function resetRenderState() {
113
113
  function pushTreeContext(totalChildren, index) {
114
114
  const st = s();
115
115
  const saved = { ...st.currentTreeContext };
116
- const pendingBits = 32 - Math.clz32(totalChildren);
116
+ const baseIdWithLeadingBit = st.currentTreeContext.id;
117
+ const baseOverflow = st.currentTreeContext.overflow;
118
+ const baseLength = 31 - Math.clz32(baseIdWithLeadingBit);
119
+ let baseId = baseIdWithLeadingBit & ~(1 << baseLength);
117
120
  const slot = index + 1;
118
- const totalBits = st.currentTreeContext.bits + pendingBits;
119
- if (totalBits <= 30) {
121
+ const newBits = 32 - Math.clz32(totalChildren);
122
+ const length = newBits + baseLength;
123
+ if (30 < length) {
124
+ const numberOfOverflowBits = baseLength - baseLength % 5;
125
+ const overflowStr = (baseId & (1 << numberOfOverflowBits) - 1).toString(32);
126
+ baseId >>= numberOfOverflowBits;
127
+ const newBaseLength = baseLength - numberOfOverflowBits;
120
128
  st.currentTreeContext = {
121
- id: st.currentTreeContext.id << pendingBits | slot,
122
- overflow: st.currentTreeContext.overflow,
123
- bits: totalBits
129
+ id: 1 << newBits + newBaseLength | slot << newBaseLength | baseId,
130
+ overflow: overflowStr + baseOverflow
124
131
  };
125
132
  } else {
126
- let newOverflow = st.currentTreeContext.overflow;
127
- if (st.currentTreeContext.bits > 0)
128
- newOverflow += st.currentTreeContext.id.toString(32);
129
- st.currentTreeContext = { id: 1 << pendingBits | slot, overflow: newOverflow, bits: pendingBits };
133
+ st.currentTreeContext = {
134
+ id: 1 << length | slot << baseLength | baseId,
135
+ overflow: baseOverflow
136
+ };
130
137
  }
131
138
  return saved;
132
139
  }
@@ -142,6 +149,9 @@ function pushComponentScope() {
142
149
  function popComponentScope(saved) {
143
150
  s().localIdCounter = saved;
144
151
  }
152
+ function componentCalledUseId() {
153
+ return s().localIdCounter > 0;
154
+ }
145
155
  function snapshotContext() {
146
156
  const st = s();
147
157
  return { tree: { ...st.currentTreeContext }, localId: st.localIdCounter };
@@ -151,6 +161,121 @@ function restoreContext(snap) {
151
161
  st.currentTreeContext = { ...snap.tree };
152
162
  st.localIdCounter = snap.localId;
153
163
  }
164
+ function getTreeId() {
165
+ const { id, overflow } = s().currentTreeContext;
166
+ if (id === 1)
167
+ return overflow;
168
+ const stripped = (id & ~(1 << 31 - Math.clz32(id))).toString(32);
169
+ return stripped + overflow;
170
+ }
171
+ function makeId() {
172
+ const st = s();
173
+ const treeId = getTreeId();
174
+ const n = st.localIdCounter++;
175
+ let id = "\xAB" + st.idPrefix + "R" + treeId;
176
+ if (n > 0)
177
+ id += "H" + n.toString(32);
178
+ return id + "\xBB";
179
+ }
180
+
181
+ // src/slim-react/hooks.ts
182
+ function useState(initialState) {
183
+ const value = typeof initialState === "function" ? initialState() : initialState;
184
+ return [value, () => {
185
+ }];
186
+ }
187
+ function useReducer(_reducer, initialState) {
188
+ return [initialState, () => {
189
+ }];
190
+ }
191
+ function useEffect(_effect, _deps) {
192
+ }
193
+ function useLayoutEffect(_effect, _deps) {
194
+ }
195
+ function useInsertionEffect(_effect, _deps) {
196
+ }
197
+ function useRef(initialValue) {
198
+ return { current: initialValue };
199
+ }
200
+ function useMemo(factory, _deps) {
201
+ return factory();
202
+ }
203
+ function useCallback(callback, _deps) {
204
+ return callback;
205
+ }
206
+ function useDebugValue(_value, _format) {
207
+ }
208
+ function useImperativeHandle(_ref, _createHandle, _deps) {
209
+ }
210
+ function useSyncExternalStore(_subscribe, getSnapshot, getServerSnapshot) {
211
+ return (getServerSnapshot || getSnapshot)();
212
+ }
213
+ function useTransition() {
214
+ return [false, (cb) => cb()];
215
+ }
216
+ function useDeferredValue(value) {
217
+ return value;
218
+ }
219
+ function useOptimistic(passthrough) {
220
+ return [passthrough, () => {
221
+ }];
222
+ }
223
+ function useActionState(_action, initialState, _permalink) {
224
+ return [initialState, () => {
225
+ }, false];
226
+ }
227
+ function use(usable) {
228
+ if (typeof usable === "object" && usable !== null && ("_currentValue" in usable || "_defaultValue" in usable)) {
229
+ return getContextValue(usable);
230
+ }
231
+ const promise = usable;
232
+ if (promise.status === "fulfilled")
233
+ return promise.value;
234
+ if (promise.status === "rejected")
235
+ throw promise.reason;
236
+ throw promise;
237
+ }
238
+
239
+ // src/slim-react/dispatcher.ts
240
+ import ReactPkg from "react";
241
+ var _internals = ReactPkg.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
242
+ var slimDispatcher = {
243
+ useId: makeId,
244
+ readContext: (ctx) => getContextValue(ctx),
245
+ useContext: (ctx) => getContextValue(ctx),
246
+ useState,
247
+ useReducer,
248
+ useEffect,
249
+ useLayoutEffect,
250
+ useInsertionEffect,
251
+ useRef,
252
+ useMemo,
253
+ useCallback,
254
+ useDebugValue,
255
+ useImperativeHandle,
256
+ useSyncExternalStore,
257
+ useTransition,
258
+ useDeferredValue,
259
+ useOptimistic,
260
+ useActionState,
261
+ use,
262
+ // React internals that compiled output may call
263
+ useMemoCache: (size) => new Array(size).fill(void 0),
264
+ useCacheRefresh: () => () => {
265
+ },
266
+ useHostTransitionStatus: () => false
267
+ };
268
+ function installDispatcher() {
269
+ if (!_internals)
270
+ return null;
271
+ const prev = _internals.H;
272
+ _internals.H = slimDispatcher;
273
+ return prev;
274
+ }
275
+ function restoreDispatcher(prev) {
276
+ if (_internals)
277
+ _internals.H = prev;
278
+ }
154
279
 
155
280
  // src/slim-react/render.ts
156
281
  var VOID_ELEMENTS = /* @__PURE__ */ new Set([
@@ -332,7 +457,11 @@ function renderAttributes(props, isSvg) {
332
457
  continue;
333
458
  }
334
459
  if (value === true) {
335
- attrs += ` ${attrName}=""`;
460
+ if (attrName.startsWith("aria-") || attrName.startsWith("data-")) {
461
+ attrs += ` ${attrName}="true"`;
462
+ } else {
463
+ attrs += ` ${attrName}=""`;
464
+ }
336
465
  continue;
337
466
  }
338
467
  if (key === "style" && typeof value === "object") {
@@ -551,6 +680,7 @@ function renderComponent(type, props, writer, isSvg) {
551
680
  return;
552
681
  }
553
682
  let result;
683
+ const prevDispatcher = installDispatcher();
554
684
  try {
555
685
  if (type.prototype && typeof type.prototype.render === "function") {
556
686
  const instance = new type(props);
@@ -564,12 +694,20 @@ function renderComponent(type, props, writer, isSvg) {
564
694
  result = type(props);
565
695
  }
566
696
  } catch (e) {
697
+ restoreDispatcher(prevDispatcher);
567
698
  popComponentScope(savedScope);
568
699
  if (isProvider)
569
700
  popContextValue(ctx, prevCtxValue);
570
701
  throw e;
571
702
  }
703
+ restoreDispatcher(prevDispatcher);
704
+ let savedIdTree;
705
+ if (!(result instanceof Promise) && componentCalledUseId()) {
706
+ savedIdTree = pushTreeContext(1, 0);
707
+ }
572
708
  const finish = () => {
709
+ if (savedIdTree !== void 0)
710
+ popTreeContext(savedIdTree);
573
711
  popComponentScope(savedScope);
574
712
  if (isProvider)
575
713
  popContextValue(ctx, prevCtxValue);
@@ -578,22 +716,33 @@ function renderComponent(type, props, writer, isSvg) {
578
716
  const m = captureMap();
579
717
  return result.then((resolved) => {
580
718
  swapContextMap(m);
719
+ let asyncSavedIdTree;
720
+ if (componentCalledUseId()) {
721
+ asyncSavedIdTree = pushTreeContext(1, 0);
722
+ }
723
+ const asyncFinish = () => {
724
+ if (asyncSavedIdTree !== void 0)
725
+ popTreeContext(asyncSavedIdTree);
726
+ popComponentScope(savedScope);
727
+ if (isProvider)
728
+ popContextValue(ctx, prevCtxValue);
729
+ };
581
730
  const r2 = renderNode(resolved, writer, isSvg);
582
731
  if (r2 && typeof r2.then === "function") {
583
732
  const m2 = captureMap();
584
733
  return r2.then(
585
734
  () => {
586
735
  swapContextMap(m2);
587
- finish();
736
+ asyncFinish();
588
737
  },
589
738
  (e) => {
590
739
  swapContextMap(m2);
591
- finish();
740
+ asyncFinish();
592
741
  throw e;
593
742
  }
594
743
  );
595
744
  }
596
- finish();
745
+ asyncFinish();
597
746
  }, (e) => {
598
747
  swapContextMap(m);
599
748
  finish();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hadars",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "Minimal SSR framework for React — rspack, HMR, TypeScript, Bun/Node/Deno",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -0,0 +1,69 @@
1
+ /**
2
+ * React dispatcher shim for slim-react SSR.
3
+ *
4
+ * During a slim-react render, `React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE.H`
5
+ * is null, so any component that calls `React.useId()` (or another hook) via
6
+ * React's own package will hit `resolveDispatcher()` → null → error.
7
+ *
8
+ * We install a minimal dispatcher object for the duration of each component
9
+ * call so that `React.useId()` routes through slim-react's tree-aware
10
+ * `makeId()`. All other hooks already have working SSR stubs in hooks.ts;
11
+ * they are forwarded here so libraries that call them via `React.*` also work.
12
+ */
13
+
14
+ import { makeId, getContextValue } from "./renderContext";
15
+ import {
16
+ useState, useReducer, useEffect, useLayoutEffect, useInsertionEffect,
17
+ useRef, useMemo, useCallback, useDebugValue, useImperativeHandle,
18
+ useSyncExternalStore, useTransition, useDeferredValue,
19
+ useOptimistic, useActionState, use,
20
+ } from "./hooks";
21
+
22
+ import ReactPkg from "react";
23
+
24
+ // React 19 exposes its shared internals under this key.
25
+ const _internals: { H: object | null } | undefined =
26
+ (ReactPkg as any).__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
27
+
28
+ // The dispatcher object we install. We keep a stable reference so the same
29
+ // object is reused across every component call.
30
+ const slimDispatcher: Record<string, unknown> = {
31
+ useId: makeId,
32
+ readContext: (ctx: any) => getContextValue(ctx),
33
+ useContext: (ctx: any) => getContextValue(ctx),
34
+ useState,
35
+ useReducer,
36
+ useEffect,
37
+ useLayoutEffect,
38
+ useInsertionEffect,
39
+ useRef,
40
+ useMemo,
41
+ useCallback,
42
+ useDebugValue,
43
+ useImperativeHandle,
44
+ useSyncExternalStore,
45
+ useTransition,
46
+ useDeferredValue,
47
+ useOptimistic,
48
+ useActionState,
49
+ use,
50
+ // React internals that compiled output may call
51
+ useMemoCache: (size: number) => new Array(size).fill(undefined),
52
+ useCacheRefresh: () => () => {},
53
+ useHostTransitionStatus: () => false,
54
+ };
55
+
56
+ /**
57
+ * Install the slim dispatcher and return the previous value.
58
+ * Call `restoreDispatcher(prev)` when the component finishes.
59
+ */
60
+ export function installDispatcher(): object | null {
61
+ if (!_internals) return null;
62
+ const prev = _internals.H;
63
+ _internals.H = slimDispatcher;
64
+ return prev;
65
+ }
66
+
67
+ export function restoreDispatcher(prev: object | null): void {
68
+ if (_internals) _internals.H = prev;
69
+ }
@@ -28,6 +28,7 @@ import {
28
28
  popTreeContext,
29
29
  pushComponentScope,
30
30
  popComponentScope,
31
+ componentCalledUseId,
31
32
  snapshotContext,
32
33
  restoreContext,
33
34
  pushContextValue,
@@ -35,7 +36,9 @@ import {
35
36
  getContextValue,
36
37
  swapContextMap,
37
38
  captureMap,
39
+ type TreeContext,
38
40
  } from "./renderContext";
41
+ import { installDispatcher, restoreDispatcher } from "./dispatcher";
39
42
 
40
43
  // ---------------------------------------------------------------------------
41
44
  // HTML helpers
@@ -287,8 +290,13 @@ function renderAttributes(props: Record<string, any>, isSvg: boolean): string {
287
290
  continue;
288
291
  }
289
292
  if (value === true) {
290
- // Emit as attr="" to match React's server output exactly.
291
- attrs += ` ${attrName}=""`;
293
+ // aria-* and data-* are string attributes: true must serialize to "true".
294
+ // HTML boolean attributes (disabled, hidden, checked, …) use attr="" (present-without-value).
295
+ if (attrName.startsWith("aria-") || attrName.startsWith("data-")) {
296
+ attrs += ` ${attrName}="true"`;
297
+ } else {
298
+ attrs += ` ${attrName}=""`;
299
+ }
292
300
  continue;
293
301
  }
294
302
  if (key === "style" && typeof value === "object") {
@@ -625,6 +633,7 @@ function renderComponent(
625
633
  }
626
634
 
627
635
  let result: SlimNode;
636
+ const prevDispatcher = installDispatcher();
628
637
  try {
629
638
  if (type.prototype && typeof type.prototype.render === "function") {
630
639
  const instance = new (type as any)(props);
@@ -638,12 +647,25 @@ function renderComponent(
638
647
  result = type(props);
639
648
  }
640
649
  } catch (e) {
650
+ restoreDispatcher(prevDispatcher);
641
651
  popComponentScope(savedScope);
642
652
  if (isProvider) popContextValue(ctx, prevCtxValue);
643
653
  throw e;
644
654
  }
655
+ restoreDispatcher(prevDispatcher);
656
+
657
+ // React 19 finishFunctionComponent: if the component called useId, push a
658
+ // tree-context slot for the component's OUTPUT children — matching React 19's
659
+ // `pushTreeContext(keyPath, 1, 0)` call inside finishFunctionComponent.
660
+ // This ensures that useId IDs produced by child components of a useId-calling
661
+ // component are tree-positioned identically to React's own renderer.
662
+ let savedIdTree: TreeContext | undefined;
663
+ if (!(result instanceof Promise) && componentCalledUseId()) {
664
+ savedIdTree = pushTreeContext(1, 0);
665
+ }
645
666
 
646
667
  const finish = () => {
668
+ if (savedIdTree !== undefined) popTreeContext(savedIdTree);
647
669
  popComponentScope(savedScope);
648
670
  if (isProvider) popContextValue(ctx, prevCtxValue);
649
671
  };
@@ -653,15 +675,25 @@ function renderComponent(
653
675
  const m = captureMap();
654
676
  return result.then((resolved) => {
655
677
  swapContextMap(m);
678
+ // Check useId after the async body has finished executing.
679
+ let asyncSavedIdTree: TreeContext | undefined;
680
+ if (componentCalledUseId()) {
681
+ asyncSavedIdTree = pushTreeContext(1, 0);
682
+ }
683
+ const asyncFinish = () => {
684
+ if (asyncSavedIdTree !== undefined) popTreeContext(asyncSavedIdTree);
685
+ popComponentScope(savedScope);
686
+ if (isProvider) popContextValue(ctx, prevCtxValue);
687
+ };
656
688
  const r = renderNode(resolved, writer, isSvg);
657
689
  if (r && typeof (r as any).then === "function") {
658
690
  const m2 = captureMap();
659
691
  return (r as Promise<void>).then(
660
- () => { swapContextMap(m2); finish(); },
661
- (e) => { swapContextMap(m2); finish(); throw e; },
692
+ () => { swapContextMap(m2); asyncFinish(); },
693
+ (e) => { swapContextMap(m2); asyncFinish(); throw e; },
662
694
  );
663
695
  }
664
- finish();
696
+ asyncFinish();
665
697
  }, (e) => { swapContextMap(m); finish(); throw e; });
666
698
  }
667
699
 
@@ -63,10 +63,15 @@ export function popContextValue(context: object, prev: unknown): void {
63
63
  (_g[MAP_KEY] as Map<object, unknown> | null)?.set(context, prev);
64
64
  }
65
65
 
66
+ // TreeContext matches React 19's representation exactly:
67
+ // `id` is a packed bitfield with a leading sentinel `1` bit followed by tree
68
+ // path slots. The most-recently-pushed slot occupies the HIGHEST non-sentinel
69
+ // bits, matching React 19's Fizz `pushTreeContext` bit-packing order.
70
+ // `overflow` accumulates segments that no longer fit in the 30-bit budget,
71
+ // prepended newest-first (same as React 19).
66
72
  export interface TreeContext {
67
- id: number;
68
- overflow: string;
69
- bits: number;
73
+ id: number; // bitfield with sentinel; 1 = empty (just sentinel, no data)
74
+ overflow: string; // base-32 partial path segments that overflowed
70
75
  }
71
76
 
72
77
  interface RenderState {
@@ -76,7 +81,8 @@ interface RenderState {
76
81
  }
77
82
 
78
83
  const GLOBAL_KEY = "__slimReactRenderState";
79
- const EMPTY: TreeContext = { id: 0, overflow: "", bits: 0 };
84
+ // React 19's initial context is { id: 1, overflow: "" } sentinel bit only.
85
+ const EMPTY: TreeContext = { id: 1, overflow: "" };
80
86
 
81
87
  function s(): RenderState {
82
88
  const g = globalThis as any;
@@ -96,23 +102,45 @@ export function setIdPrefix(prefix: string) {
96
102
  s().idPrefix = prefix;
97
103
  }
98
104
 
105
+ /**
106
+ * Push a new level onto the tree context — matches React 19's Fizz
107
+ * `pushTreeContext` exactly:
108
+ * - new slot occupies the HIGHER bit positions (above the old base data)
109
+ * - on overflow, the LOWEST bits of the old data move to the overflow string
110
+ * (rounded to a multiple of 5 so base-32 digits align on byte boundaries)
111
+ */
99
112
  export function pushTreeContext(totalChildren: number, index: number): TreeContext {
100
113
  const st = s();
101
114
  const saved: TreeContext = { ...st.currentTreeContext };
102
- const pendingBits = 32 - Math.clz32(totalChildren);
103
- const slot = index + 1;
104
- const totalBits = st.currentTreeContext.bits + pendingBits;
105
115
 
106
- if (totalBits <= 30) {
116
+ const baseIdWithLeadingBit = st.currentTreeContext.id;
117
+ const baseOverflow = st.currentTreeContext.overflow;
118
+ // Number of data bits currently stored (excludes the sentinel bit).
119
+ const baseLength = 31 - Math.clz32(baseIdWithLeadingBit);
120
+ // Strip the sentinel to get the pure data portion.
121
+ let baseId = baseIdWithLeadingBit & ~(1 << baseLength);
122
+
123
+ const slot = index + 1; // 1-indexed
124
+ const newBits = 32 - Math.clz32(totalChildren); // bits required for the new slot
125
+ const length = newBits + baseLength; // total data bits after push
126
+
127
+ if (30 < length) {
128
+ // Overflow: flush the lowest bits of the old data to the overflow string.
129
+ // Round down to a multiple of 5 so each base-32 character covers exactly
130
+ // 5 bits (no fractional digits that would corrupt adjacent chars).
131
+ const numberOfOverflowBits = baseLength - (baseLength % 5);
132
+ const overflowStr = (baseId & ((1 << numberOfOverflowBits) - 1)).toString(32);
133
+ baseId >>= numberOfOverflowBits;
134
+ const newBaseLength = baseLength - numberOfOverflowBits;
107
135
  st.currentTreeContext = {
108
- id: (st.currentTreeContext.id << pendingBits) | slot,
109
- overflow: st.currentTreeContext.overflow,
110
- bits: totalBits,
136
+ id: (1 << (newBits + newBaseLength)) | (slot << newBaseLength) | baseId,
137
+ overflow: overflowStr + baseOverflow,
111
138
  };
112
139
  } else {
113
- let newOverflow = st.currentTreeContext.overflow;
114
- if (st.currentTreeContext.bits > 0) newOverflow += st.currentTreeContext.id.toString(32);
115
- st.currentTreeContext = { id: (1 << pendingBits) | slot, overflow: newOverflow, bits: pendingBits };
140
+ st.currentTreeContext = {
141
+ id: (1 << length) | (slot << baseLength) | baseId,
142
+ overflow: baseOverflow,
143
+ };
116
144
  }
117
145
  return saved;
118
146
  }
@@ -132,6 +160,11 @@ export function popComponentScope(saved: number) {
132
160
  s().localIdCounter = saved;
133
161
  }
134
162
 
163
+ /** True if the current component has called useId at least once. */
164
+ export function componentCalledUseId(): boolean {
165
+ return s().localIdCounter > 0;
166
+ }
167
+
135
168
  export function snapshotContext(): { tree: TreeContext; localId: number } {
136
169
  const st = s();
137
170
  return { tree: { ...st.currentTreeContext }, localId: st.localIdCounter };
@@ -143,18 +176,36 @@ export function restoreContext(snap: { tree: TreeContext; localId: number }) {
143
176
  st.localIdCounter = snap.localId;
144
177
  }
145
178
 
179
+ /**
180
+ * Produce the base-32 tree path string from the current context.
181
+ * Strips the sentinel bit then concatenates stripped_id + overflow —
182
+ * the same formula React 19 uses in both its Fizz SSR renderer and
183
+ * the client-side `mountId`.
184
+ */
146
185
  function getTreeId(): string {
147
- const { id, overflow, bits } = s().currentTreeContext;
148
- return bits > 0 ? overflow + id.toString(32) : overflow;
186
+ const { id, overflow } = s().currentTreeContext;
187
+ if (id === 1) return overflow; // sentinel only → no local path segment
188
+ const stripped = (id & ~(1 << (31 - Math.clz32(id)))).toString(32);
189
+ return stripped + overflow;
149
190
  }
150
191
 
192
+ /**
193
+ * Generate a `useId`-compatible ID for the current call site.
194
+ *
195
+ * Format: `«<idPrefix>R<treeId>»` (React 19.1+)
196
+ * with an optional `H<n>` suffix for the n-th useId call in the same
197
+ * component (matching React 19's `localIdCounter` behaviour).
198
+ *
199
+ * React 19.1 switched from `_R_<id>_` to `«R<id>»` (U+00AB / U+00BB).
200
+ * This matches React 19.1's `mountId` output on the Fizz SSR renderer and
201
+ * the client hydration path, so the IDs produced here will agree with the
202
+ * real React runtime during `hydrateRoot`.
203
+ */
151
204
  export function makeId(): string {
152
205
  const st = s();
153
206
  const treeId = getTreeId();
154
207
  const n = st.localIdCounter++;
155
- let id = ":" + st.idPrefix + "R";
156
- if (treeId.length > 0) id += treeId;
157
- id += ":";
158
- if (n > 0) id += "H" + n.toString(32) + ":";
159
- return id;
208
+ let id = "\u00ab" + st.idPrefix + "R" + treeId;
209
+ if (n > 0) id += "H" + n.toString(32);
210
+ return id + "\u00bb";
160
211
  }