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 +50 -17
- package/dist/index.d.ts +18 -0
- package/dist/slim-react/index.cjs +31 -20
- package/dist/slim-react/index.js +31 -20
- package/dist/ssr-render-worker.js +23 -12
- package/dist/ssr-watch.js +25 -5
- package/package.json +1 -1
- package/src/build.ts +3 -1
- package/src/slim-react/render.ts +7 -2
- package/src/slim-react/renderContext.ts +66 -21
- package/src/ssr-watch.ts +1 -1
- package/src/types/hadars.ts +18 -0
- package/src/utils/rspack.ts +37 -6
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:
|
|
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
|
|
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
|
|
212
|
-
|
|
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:
|
|
215
|
-
overflow:
|
|
216
|
-
bits: totalBits
|
|
222
|
+
id: 1 << newBits + newBaseLength | slot << newBaseLength | baseId,
|
|
223
|
+
overflow: overflowStr + baseOverflow
|
|
217
224
|
};
|
|
218
225
|
} else {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
152
|
-
|
|
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:
|
|
155
|
-
overflow:
|
|
156
|
-
bits: totalBits
|
|
162
|
+
id: 1 << newBits + newBaseLength | slot << newBaseLength | baseId,
|
|
163
|
+
overflow: overflowStr + baseOverflow
|
|
157
164
|
};
|
|
158
165
|
} else {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
189
|
-
|
|
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 = "
|
|
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
|
-
|
|
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") {
|
package/dist/slim-react/index.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
61
|
-
|
|
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:
|
|
64
|
-
overflow:
|
|
65
|
-
bits: totalBits
|
|
71
|
+
id: 1 << newBits + newBaseLength | slot << newBaseLength | baseId,
|
|
72
|
+
overflow: overflowStr + baseOverflow
|
|
66
73
|
};
|
|
67
74
|
} else {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
98
|
-
|
|
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 = "
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
119
|
-
|
|
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:
|
|
122
|
-
overflow:
|
|
123
|
-
bits: totalBits
|
|
129
|
+
id: 1 << newBits + newBaseLength | slot << newBaseLength | baseId,
|
|
130
|
+
overflow: overflowStr + baseOverflow
|
|
124
131
|
};
|
|
125
132
|
} else {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
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), {
|
package/src/slim-react/render.ts
CHANGED
|
@@ -287,8 +287,13 @@ function renderAttributes(props: Record<string, any>, isSvg: boolean): string {
|
|
|
287
287
|
continue;
|
|
288
288
|
}
|
|
289
289
|
if (value === true) {
|
|
290
|
-
//
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: (
|
|
109
|
-
overflow:
|
|
110
|
-
bits: totalBits,
|
|
136
|
+
id: (1 << (newBits + newBaseLength)) | (slot << newBaseLength) | baseId,
|
|
137
|
+
overflow: overflowStr + baseOverflow,
|
|
111
138
|
};
|
|
112
139
|
} else {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
148
|
-
|
|
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 = "
|
|
156
|
-
if (
|
|
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');
|
package/src/types/hadars.ts
CHANGED
|
@@ -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.
|
package/src/utils/rspack.ts
CHANGED
|
@@ -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
|
-
|
|
237
|
-
|
|
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(
|
|
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
|
|