hadars 0.1.26 → 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") {
@@ -1138,13 +1149,33 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
1138
1149
  // and does not call React hooks, so it is safe to leave as external.
1139
1150
  "@emotion/server"
1140
1151
  ] : void 0;
1152
+ const effectiveReactDev = isServerBuild ? false : opts.reactMode === "development" ? true : opts.reactMode === "production" ? false : isDev;
1153
+ if (!isServerBuild && opts.reactMode !== void 0) {
1154
+ const rules = localConfig.module?.rules ?? [];
1155
+ for (const rule of rules) {
1156
+ if (!rule?.use || !Array.isArray(rule.use))
1157
+ continue;
1158
+ for (const entry2 of rule.use) {
1159
+ if (entry2?.loader?.includes("swc-loader")) {
1160
+ entry2.options = entry2.options ?? {};
1161
+ entry2.options.jsc = entry2.options.jsc ?? {};
1162
+ entry2.options.jsc.transform = entry2.options.jsc.transform ?? {};
1163
+ entry2.options.jsc.transform.react = entry2.options.jsc.transform.react ?? {};
1164
+ entry2.options.jsc.transform.react.development = effectiveReactDev;
1165
+ entry2.options.jsc.transform.react.refresh = effectiveReactDev && isDev;
1166
+ }
1167
+ }
1168
+ }
1169
+ }
1141
1170
  const extraPlugins = [];
1142
- if (opts.define && typeof opts.define === "object") {
1171
+ const defineValues = { ...opts.define ?? {} };
1172
+ if (!isServerBuild && opts.reactMode !== void 0) {
1173
+ defineValues["process.env.NODE_ENV"] = JSON.stringify(opts.reactMode);
1174
+ }
1175
+ if (Object.keys(defineValues).length > 0) {
1143
1176
  const DefinePlugin = rspack.DefinePlugin || rspack.plugins?.DefinePlugin;
1144
1177
  if (DefinePlugin) {
1145
- extraPlugins.push(new DefinePlugin(opts.define));
1146
- } else {
1147
- extraPlugins.push({ name: "DefinePlugin", value: opts.define });
1178
+ extraPlugins.push(new DefinePlugin(defineValues));
1148
1179
  }
1149
1180
  }
1150
1181
  const resolveConfig = {
@@ -1875,6 +1906,7 @@ var dev = async (options) => {
1875
1906
  swcPlugins: options.swcPlugins,
1876
1907
  define: options.define,
1877
1908
  moduleRules: options.moduleRules,
1909
+ reactMode: options.reactMode,
1878
1910
  htmlTemplate: resolvedHtmlTemplate
1879
1911
  });
1880
1912
  const devServer = new RspackDevServer({
@@ -1912,7 +1944,7 @@ var dev = async (options) => {
1912
1944
  `--base=${baseURL}`,
1913
1945
  ...options.swcPlugins ? [`--swcPlugins=${JSON.stringify(options.swcPlugins)}`] : [],
1914
1946
  ...options.define ? [`--define=${JSON.stringify(options.define)}`] : [],
1915
- ...options.moduleRules ? [`--moduleRules=${JSON.stringify(options.moduleRules)}`] : []
1947
+ ...options.moduleRules ? [`--moduleRules=${JSON.stringify(options.moduleRules, (_k, v) => v instanceof RegExp ? { __re: v.source, __flags: v.flags } : v)}`] : []
1916
1948
  ], { stdio: "pipe" });
1917
1949
  child.stdin?.end();
1918
1950
  const cleanupChild = () => {
@@ -2094,6 +2126,7 @@ var build = async (options) => {
2094
2126
  define: options.define,
2095
2127
  moduleRules: options.moduleRules,
2096
2128
  optimization: options.optimization,
2129
+ reactMode: options.reactMode,
2097
2130
  htmlTemplate: resolvedHtmlTemplate
2098
2131
  }),
2099
2132
  compileEntry(pathMod2.resolve(__dirname2, options.entry), {
package/dist/index.d.ts CHANGED
@@ -111,6 +111,24 @@ interface HadarsOptions {
111
111
  * Note: inline styles are processed once at startup and are not live-reloaded.
112
112
  */
113
113
  htmlTemplate?: string;
114
+ /**
115
+ * Force the React runtime mode independently of the build mode.
116
+ * Useful when you need production build optimizations (minification, tree-shaking)
117
+ * but want React's development build for debugging hydration mismatches or
118
+ * component stack traces.
119
+ *
120
+ * - `'development'` — forces `process.env.NODE_ENV = "development"` and enables
121
+ * JSX source info even in `hadars build`. React prints detailed hydration error
122
+ * messages and component stacks.
123
+ * - `'production'` — the default; React uses the optimised production bundle.
124
+ *
125
+ * Only affects the **client** bundle. The SSR bundle always uses slim-react.
126
+ *
127
+ * @example
128
+ * // hadars.config.ts — debug hydration errors in a production build
129
+ * reactMode: 'development'
130
+ */
131
+ reactMode?: 'development' | 'production';
114
132
  /**
115
133
  * Additional rspack module rules appended to the built-in rule set.
116
134
  * Applied to both the client and the SSR bundle.
@@ -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/dist/ssr-watch.js CHANGED
@@ -188,13 +188,33 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
188
188
  // and does not call React hooks, so it is safe to leave as external.
189
189
  "@emotion/server"
190
190
  ] : void 0;
191
+ const effectiveReactDev = isServerBuild ? false : opts.reactMode === "development" ? true : opts.reactMode === "production" ? false : isDev;
192
+ if (!isServerBuild && opts.reactMode !== void 0) {
193
+ const rules = localConfig.module?.rules ?? [];
194
+ for (const rule of rules) {
195
+ if (!rule?.use || !Array.isArray(rule.use))
196
+ continue;
197
+ for (const entry3 of rule.use) {
198
+ if (entry3?.loader?.includes("swc-loader")) {
199
+ entry3.options = entry3.options ?? {};
200
+ entry3.options.jsc = entry3.options.jsc ?? {};
201
+ entry3.options.jsc.transform = entry3.options.jsc.transform ?? {};
202
+ entry3.options.jsc.transform.react = entry3.options.jsc.transform.react ?? {};
203
+ entry3.options.jsc.transform.react.development = effectiveReactDev;
204
+ entry3.options.jsc.transform.react.refresh = effectiveReactDev && isDev;
205
+ }
206
+ }
207
+ }
208
+ }
191
209
  const extraPlugins = [];
192
- if (opts.define && typeof opts.define === "object") {
210
+ const defineValues = { ...opts.define ?? {} };
211
+ if (!isServerBuild && opts.reactMode !== void 0) {
212
+ defineValues["process.env.NODE_ENV"] = JSON.stringify(opts.reactMode);
213
+ }
214
+ if (Object.keys(defineValues).length > 0) {
193
215
  const DefinePlugin = rspack.DefinePlugin || rspack.plugins?.DefinePlugin;
194
216
  if (DefinePlugin) {
195
- extraPlugins.push(new DefinePlugin(opts.define));
196
- } else {
197
- extraPlugins.push({ name: "DefinePlugin", value: opts.define });
217
+ extraPlugins.push(new DefinePlugin(defineValues));
198
218
  }
199
219
  }
200
220
  const resolveConfig = {
@@ -354,7 +374,7 @@ var outFile = argv["outFile"] || "index.ssr.js";
354
374
  var base = argv["base"] || "";
355
375
  var swcPlugins = argv["swcPlugins"] ? JSON.parse(argv["swcPlugins"]) : void 0;
356
376
  var define = argv["define"] ? JSON.parse(argv["define"]) : void 0;
357
- var moduleRules = argv["moduleRules"] ? JSON.parse(argv["moduleRules"]) : void 0;
377
+ var moduleRules = argv["moduleRules"] ? JSON.parse(argv["moduleRules"], (_k, v) => v && typeof v === "object" && "__re" in v ? new RegExp(v.__re, v.__flags) : v) : void 0;
358
378
  if (!entry) {
359
379
  console.error("ssr-watch: missing --entry argument");
360
380
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hadars",
3
- "version": "0.1.26",
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",
package/src/build.ts CHANGED
@@ -508,6 +508,7 @@ export const dev = async (options: HadarsRuntimeOptions) => {
508
508
  swcPlugins: options.swcPlugins,
509
509
  define: options.define,
510
510
  moduleRules: options.moduleRules,
511
+ reactMode: options.reactMode,
511
512
  htmlTemplate: resolvedHtmlTemplate,
512
513
  });
513
514
 
@@ -555,7 +556,7 @@ export const dev = async (options: HadarsRuntimeOptions) => {
555
556
  `--base=${baseURL}`,
556
557
  ...(options.swcPlugins ? [`--swcPlugins=${JSON.stringify(options.swcPlugins)}`] : []),
557
558
  ...(options.define ? [`--define=${JSON.stringify(options.define)}`] : []),
558
- ...(options.moduleRules ? [`--moduleRules=${JSON.stringify(options.moduleRules)}`] : []),
559
+ ...(options.moduleRules ? [`--moduleRules=${JSON.stringify(options.moduleRules, (_k, v) => v instanceof RegExp ? { __re: v.source, __flags: v.flags } : v)}`] : []),
559
560
  ], { stdio: 'pipe' });
560
561
  child.stdin?.end();
561
562
 
@@ -751,6 +752,7 @@ export const build = async (options: HadarsRuntimeOptions) => {
751
752
  define: options.define,
752
753
  moduleRules: options.moduleRules,
753
754
  optimization: options.optimization,
755
+ reactMode: options.reactMode,
754
756
  htmlTemplate: resolvedHtmlTemplate,
755
757
  }),
756
758
  compileEntry(pathMod.resolve(__dirname, options.entry), {
@@ -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
  }
package/src/ssr-watch.ts CHANGED
@@ -21,7 +21,7 @@ const outFile = argv['outFile'] || 'index.ssr.js';
21
21
  const base = argv['base'] || '';
22
22
  const swcPlugins = argv['swcPlugins'] ? JSON.parse(argv['swcPlugins']) : undefined;
23
23
  const define = argv['define'] ? JSON.parse(argv['define']) : undefined;
24
- const moduleRules = argv['moduleRules'] ? JSON.parse(argv['moduleRules']) : undefined;
24
+ const moduleRules = argv['moduleRules'] ? JSON.parse(argv['moduleRules'], (_k, v) => (v && typeof v === 'object' && '__re' in v) ? new RegExp(v.__re, v.__flags) : v) : undefined;
25
25
 
26
26
  if (!entry) {
27
27
  console.error('ssr-watch: missing --entry argument');
@@ -111,6 +111,24 @@ export interface HadarsOptions {
111
111
  * Note: inline styles are processed once at startup and are not live-reloaded.
112
112
  */
113
113
  htmlTemplate?: string;
114
+ /**
115
+ * Force the React runtime mode independently of the build mode.
116
+ * Useful when you need production build optimizations (minification, tree-shaking)
117
+ * but want React's development build for debugging hydration mismatches or
118
+ * component stack traces.
119
+ *
120
+ * - `'development'` — forces `process.env.NODE_ENV = "development"` and enables
121
+ * JSX source info even in `hadars build`. React prints detailed hydration error
122
+ * messages and component stacks.
123
+ * - `'production'` — the default; React uses the optimised production bundle.
124
+ *
125
+ * Only affects the **client** bundle. The SSR bundle always uses slim-react.
126
+ *
127
+ * @example
128
+ * // hadars.config.ts — debug hydration errors in a production build
129
+ * reactMode: 'development'
130
+ */
131
+ reactMode?: 'development' | 'production';
114
132
  /**
115
133
  * Additional rspack module rules appended to the built-in rule set.
116
134
  * Applied to both the client and the SSR bundle.
@@ -134,6 +134,8 @@ interface EntryOptions {
134
134
  optimization?: Record<string, unknown>;
135
135
  // additional module rules appended after the built-in rules
136
136
  moduleRules?: Record<string, any>[];
137
+ // force React runtime mode independently of build mode (client only)
138
+ reactMode?: 'development' | 'production';
137
139
  }
138
140
 
139
141
  const buildCompilerConfig = (
@@ -232,15 +234,44 @@ const buildCompilerConfig = (
232
234
  '@emotion/server',
233
235
  ] : undefined;
234
236
 
237
+ // reactMode lets the caller force React's dev/prod runtime independently of
238
+ // the webpack build mode. Only applies to the client bundle (SSR uses slim-react).
239
+ // 'development' → process.env.NODE_ENV = "development" + JSX dev transform.
240
+ const effectiveReactDev = isServerBuild
241
+ ? false // slim-react doesn't use NODE_ENV
242
+ : opts.reactMode === 'development' ? true
243
+ : opts.reactMode === 'production' ? false
244
+ : isDev; // default: follow build mode
245
+
246
+ if (!isServerBuild && opts.reactMode !== undefined) {
247
+ // Override the SWC JSX development flag for all js/ts rules already built
248
+ const rules = localConfig.module?.rules ?? [];
249
+ for (const rule of rules) {
250
+ if (!rule?.use || !Array.isArray(rule.use)) continue;
251
+ for (const entry of rule.use) {
252
+ if (entry?.loader?.includes('swc-loader')) {
253
+ entry.options = entry.options ?? {};
254
+ entry.options.jsc = entry.options.jsc ?? {};
255
+ entry.options.jsc.transform = entry.options.jsc.transform ?? {};
256
+ entry.options.jsc.transform.react = entry.options.jsc.transform.react ?? {};
257
+ entry.options.jsc.transform.react.development = effectiveReactDev;
258
+ entry.options.jsc.transform.react.refresh = effectiveReactDev && isDev;
259
+ }
260
+ }
261
+ }
262
+ }
263
+
235
264
  const extraPlugins: any[] = [];
236
- if (opts.define && typeof opts.define === 'object') {
237
- // rspack's DefinePlugin shape mirrors webpack's DefinePlugin
265
+ const defineValues: Record<string, string> = { ...(opts.define ?? {}) };
266
+ // When reactMode overrides the React runtime we must also set process.env.NODE_ENV
267
+ // so React picks its dev/prod bundle, independently of the rspack build mode.
268
+ if (!isServerBuild && opts.reactMode !== undefined) {
269
+ defineValues['process.env.NODE_ENV'] = JSON.stringify(opts.reactMode);
270
+ }
271
+ if (Object.keys(defineValues).length > 0) {
238
272
  const DefinePlugin = (rspack as any).DefinePlugin || (rspack as any).plugins?.DefinePlugin;
239
273
  if (DefinePlugin) {
240
- extraPlugins.push(new DefinePlugin(opts.define));
241
- } else {
242
- // fallback: try to inject via plugin API name
243
- extraPlugins.push({ name: 'DefinePlugin', value: opts.define });
274
+ extraPlugins.push(new DefinePlugin(defineValues));
244
275
  }
245
276
  }
246
277