hadars 0.1.27 → 0.1.28

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
  }
@@ -425,7 +432,11 @@ function renderAttributes(props, isSvg) {
425
432
  continue;
426
433
  }
427
434
  if (value === true) {
428
- attrs += ` ${attrName}=""`;
435
+ if (attrName.startsWith("aria-") || attrName.startsWith("data-")) {
436
+ attrs += ` ${attrName}="true"`;
437
+ } else {
438
+ attrs += ` ${attrName}=""`;
439
+ }
429
440
  continue;
430
441
  }
431
442
  if (key === "style" && typeof value === "object") {
@@ -130,7 +130,7 @@ function popContextValue(context, prev) {
130
130
  _g[MAP_KEY]?.set(context, prev);
131
131
  }
132
132
  var GLOBAL_KEY = "__slimReactRenderState";
133
- var EMPTY = { id: 0, overflow: "", bits: 0 };
133
+ var EMPTY = { id: 1, overflow: "" };
134
134
  function s() {
135
135
  const g = globalThis;
136
136
  if (!g[GLOBAL_KEY]) {
@@ -146,20 +146,27 @@ function resetRenderState() {
146
146
  function pushTreeContext(totalChildren, index) {
147
147
  const st = s();
148
148
  const saved = { ...st.currentTreeContext };
149
- const pendingBits = 32 - Math.clz32(totalChildren);
149
+ const baseIdWithLeadingBit = st.currentTreeContext.id;
150
+ const baseOverflow = st.currentTreeContext.overflow;
151
+ const baseLength = 31 - Math.clz32(baseIdWithLeadingBit);
152
+ let baseId = baseIdWithLeadingBit & ~(1 << baseLength);
150
153
  const slot = index + 1;
151
- const totalBits = st.currentTreeContext.bits + pendingBits;
152
- if (totalBits <= 30) {
154
+ const newBits = 32 - Math.clz32(totalChildren);
155
+ const length = newBits + baseLength;
156
+ if (30 < length) {
157
+ const numberOfOverflowBits = baseLength - baseLength % 5;
158
+ const overflowStr = (baseId & (1 << numberOfOverflowBits) - 1).toString(32);
159
+ baseId >>= numberOfOverflowBits;
160
+ const newBaseLength = baseLength - numberOfOverflowBits;
153
161
  st.currentTreeContext = {
154
- id: st.currentTreeContext.id << pendingBits | slot,
155
- overflow: st.currentTreeContext.overflow,
156
- bits: totalBits
162
+ id: 1 << newBits + newBaseLength | slot << newBaseLength | baseId,
163
+ overflow: overflowStr + baseOverflow
157
164
  };
158
165
  } 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 };
166
+ st.currentTreeContext = {
167
+ id: 1 << length | slot << baseLength | baseId,
168
+ overflow: baseOverflow
169
+ };
163
170
  }
164
171
  return saved;
165
172
  }
@@ -185,20 +192,20 @@ function restoreContext(snap) {
185
192
  st.localIdCounter = snap.localId;
186
193
  }
187
194
  function getTreeId() {
188
- const { id, overflow, bits } = s().currentTreeContext;
189
- return bits > 0 ? overflow + id.toString(32) : overflow;
195
+ const { id, overflow } = s().currentTreeContext;
196
+ if (id === 1)
197
+ return overflow;
198
+ const stripped = (id & ~(1 << 31 - Math.clz32(id))).toString(32);
199
+ return stripped + overflow;
190
200
  }
191
201
  function makeId() {
192
202
  const st = s();
193
203
  const treeId = getTreeId();
194
204
  const n = st.localIdCounter++;
195
- let id = ":" + st.idPrefix + "R";
196
- if (treeId.length > 0)
197
- id += treeId;
198
- id += ":";
205
+ let id = "_" + st.idPrefix + "R_" + treeId;
199
206
  if (n > 0)
200
- id += "H" + n.toString(32) + ":";
201
- return id;
207
+ id += "H" + n.toString(32);
208
+ return id + "_";
202
209
  }
203
210
 
204
211
  // src/slim-react/hooks.ts
@@ -471,7 +478,11 @@ function renderAttributes(props, isSvg) {
471
478
  continue;
472
479
  }
473
480
  if (value === true) {
474
- attrs += ` ${attrName}=""`;
481
+ if (attrName.startsWith("aria-") || attrName.startsWith("data-")) {
482
+ attrs += ` ${attrName}="true"`;
483
+ } else {
484
+ attrs += ` ${attrName}=""`;
485
+ }
475
486
  continue;
476
487
  }
477
488
  if (key === "style" && typeof value === "object") {
@@ -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
  }
@@ -94,20 +101,20 @@ function restoreContext(snap) {
94
101
  st.localIdCounter = snap.localId;
95
102
  }
96
103
  function getTreeId() {
97
- const { id, overflow, bits } = s().currentTreeContext;
98
- return bits > 0 ? overflow + id.toString(32) : overflow;
104
+ const { id, overflow } = s().currentTreeContext;
105
+ if (id === 1)
106
+ return overflow;
107
+ const stripped = (id & ~(1 << 31 - Math.clz32(id))).toString(32);
108
+ return stripped + overflow;
99
109
  }
100
110
  function makeId() {
101
111
  const st = s();
102
112
  const treeId = getTreeId();
103
113
  const n = st.localIdCounter++;
104
- let id = ":" + st.idPrefix + "R";
105
- if (treeId.length > 0)
106
- id += treeId;
107
- id += ":";
114
+ let id = "_" + st.idPrefix + "R_" + treeId;
108
115
  if (n > 0)
109
- id += "H" + n.toString(32) + ":";
110
- return id;
116
+ id += "H" + n.toString(32);
117
+ return id + "_";
111
118
  }
112
119
 
113
120
  // src/slim-react/hooks.ts
@@ -380,7 +387,11 @@ function renderAttributes(props, isSvg) {
380
387
  continue;
381
388
  }
382
389
  if (value === true) {
383
- attrs += ` ${attrName}=""`;
390
+ if (attrName.startsWith("aria-") || attrName.startsWith("data-")) {
391
+ attrs += ` ${attrName}="true"`;
392
+ } else {
393
+ attrs += ` ${attrName}=""`;
394
+ }
384
395
  continue;
385
396
  }
386
397
  if (key === "style" && typeof value === "object") {
@@ -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
  }
@@ -332,7 +339,11 @@ function renderAttributes(props, isSvg) {
332
339
  continue;
333
340
  }
334
341
  if (value === true) {
335
- attrs += ` ${attrName}=""`;
342
+ if (attrName.startsWith("aria-") || attrName.startsWith("data-")) {
343
+ attrs += ` ${attrName}="true"`;
344
+ } else {
345
+ attrs += ` ${attrName}=""`;
346
+ }
336
347
  continue;
337
348
  }
338
349
  if (key === "style" && typeof value === "object") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hadars",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "Minimal SSR framework for React — rspack, HMR, TypeScript, Bun/Node/Deno",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -287,8 +287,13 @@ function renderAttributes(props: Record<string, any>, isSvg: boolean): string {
287
287
  continue;
288
288
  }
289
289
  if (value === true) {
290
- // Emit as attr="" to match React's server output exactly.
291
- attrs += ` ${attrName}=""`;
290
+ // aria-* and data-* are string attributes: true must serialize to "true".
291
+ // HTML boolean attributes (disabled, hidden, checked, …) use attr="" (present-without-value).
292
+ if (attrName.startsWith("aria-") || attrName.startsWith("data-")) {
293
+ attrs += ` ${attrName}="true"`;
294
+ } else {
295
+ attrs += ` ${attrName}=""`;
296
+ }
292
297
  continue;
293
298
  }
294
299
  if (key === "style" && typeof value === "object") {
@@ -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
  }
@@ -143,18 +171,35 @@ export function restoreContext(snap: { tree: TreeContext; localId: number }) {
143
171
  st.localIdCounter = snap.localId;
144
172
  }
145
173
 
174
+ /**
175
+ * Produce the base-32 tree path string from the current context.
176
+ * Strips the sentinel bit then concatenates stripped_id + overflow —
177
+ * the same formula React 19 uses in both its Fizz SSR renderer and
178
+ * the client-side `mountId`.
179
+ */
146
180
  function getTreeId(): string {
147
- const { id, overflow, bits } = s().currentTreeContext;
148
- return bits > 0 ? overflow + id.toString(32) : overflow;
181
+ const { id, overflow } = s().currentTreeContext;
182
+ if (id === 1) return overflow; // sentinel only → no local path segment
183
+ const stripped = (id & ~(1 << (31 - Math.clz32(id)))).toString(32);
184
+ return stripped + overflow;
149
185
  }
150
186
 
187
+ /**
188
+ * Generate a `useId`-compatible ID for the current call site.
189
+ *
190
+ * Format: `_<idPrefix>R_<treeId>_`
191
+ * with an optional `H<n>` suffix when the same component calls useId
192
+ * more than once (matching React 19's `localIdCounter` behaviour).
193
+ *
194
+ * This matches React 19's `mountId` output on both the Fizz SSR renderer
195
+ * and the client hydration path, so the IDs produced here will agree with
196
+ * the real React runtime during `hydrateRoot`.
197
+ */
151
198
  export function makeId(): string {
152
199
  const st = s();
153
200
  const treeId = getTreeId();
154
201
  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;
202
+ let id = "_" + st.idPrefix + "R_" + treeId;
203
+ if (n > 0) id += "H" + n.toString(32);
204
+ return id + "_";
160
205
  }