hadars 0.1.25 → 0.1.27

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
@@ -1121,12 +1121,11 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
1121
1121
  // Route all React imports to slim-react for SSR.
1122
1122
  react: slimReactIndex,
1123
1123
  "react/jsx-runtime": slimReactJsx,
1124
- "react/jsx-dev-runtime": slimReactJsx,
1125
- // Keep emotion on the project's node_modules (server-safe entry).
1126
- "@emotion/react": path.resolve(process.cwd(), "node_modules", "@emotion", "react"),
1127
- "@emotion/server": path.resolve(process.cwd(), "node_modules", "@emotion", "server"),
1128
- "@emotion/cache": path.resolve(process.cwd(), "node_modules", "@emotion", "cache"),
1129
- "@emotion/styled": path.resolve(process.cwd(), "node_modules", "@emotion", "styled")
1124
+ "react/jsx-dev-runtime": slimReactJsx
1125
+ // @emotion/* is bundled (not external) so that its `react` imports are
1126
+ // resolved through the alias above to slim-react. If left external,
1127
+ // emotion loads real React from node_modules and calls
1128
+ // ReactSharedInternals.H.useContext which requires React's dispatcher.
1130
1129
  } : void 0;
1131
1130
  const externals = isServerBuild ? [
1132
1131
  // Node.js built-ins — must not be bundled; resolved by the runtime.
@@ -1135,27 +1134,47 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
1135
1134
  "node:os",
1136
1135
  "node:stream",
1137
1136
  "node:util",
1138
- // react / react-dom are replaced by slim-react via alias above — not external.
1139
- // emotion should be external on server builds to avoid client/browser code
1140
- "@emotion/react",
1141
- "@emotion/server",
1142
- "@emotion/cache",
1143
- "@emotion/styled"
1137
+ // @emotion/server is only used outside component rendering (CSS extraction)
1138
+ // and does not call React hooks, so it is safe to leave as external.
1139
+ "@emotion/server"
1144
1140
  ] : void 0;
1141
+ const effectiveReactDev = isServerBuild ? false : opts.reactMode === "development" ? true : opts.reactMode === "production" ? false : isDev;
1142
+ if (!isServerBuild && opts.reactMode !== void 0) {
1143
+ const rules = localConfig.module?.rules ?? [];
1144
+ for (const rule of rules) {
1145
+ if (!rule?.use || !Array.isArray(rule.use))
1146
+ continue;
1147
+ for (const entry2 of rule.use) {
1148
+ if (entry2?.loader?.includes("swc-loader")) {
1149
+ entry2.options = entry2.options ?? {};
1150
+ entry2.options.jsc = entry2.options.jsc ?? {};
1151
+ entry2.options.jsc.transform = entry2.options.jsc.transform ?? {};
1152
+ entry2.options.jsc.transform.react = entry2.options.jsc.transform.react ?? {};
1153
+ entry2.options.jsc.transform.react.development = effectiveReactDev;
1154
+ entry2.options.jsc.transform.react.refresh = effectiveReactDev && isDev;
1155
+ }
1156
+ }
1157
+ }
1158
+ }
1145
1159
  const extraPlugins = [];
1146
- if (opts.define && typeof opts.define === "object") {
1160
+ const defineValues = { ...opts.define ?? {} };
1161
+ if (!isServerBuild && opts.reactMode !== void 0) {
1162
+ defineValues["process.env.NODE_ENV"] = JSON.stringify(opts.reactMode);
1163
+ }
1164
+ if (Object.keys(defineValues).length > 0) {
1147
1165
  const DefinePlugin = rspack.DefinePlugin || rspack.plugins?.DefinePlugin;
1148
1166
  if (DefinePlugin) {
1149
- extraPlugins.push(new DefinePlugin(opts.define));
1150
- } else {
1151
- extraPlugins.push({ name: "DefinePlugin", value: opts.define });
1167
+ extraPlugins.push(new DefinePlugin(defineValues));
1152
1168
  }
1153
1169
  }
1154
1170
  const resolveConfig = {
1155
1171
  extensions: [".tsx", ".ts", ".js", ".jsx"],
1156
1172
  alias: resolveAliases,
1157
1173
  // for server builds prefer the package "main"/"module" fields and avoid "browser" so we don't pick browser-specific entrypoints
1158
- mainFields: isServerBuild ? ["main", "module"] : ["browser", "module", "main"]
1174
+ mainFields: isServerBuild ? ["main", "module"] : ["browser", "module", "main"],
1175
+ // for server builds exclude the "browser" condition so packages with package.json
1176
+ // "exports" conditions (e.g. @emotion/*) resolve their Node/CJS entry, not the browser build
1177
+ ...isServerBuild ? { conditionNames: ["node", "require", "default"] } : {}
1159
1178
  };
1160
1179
  const optimization = !isServerBuild && !isDev ? {
1161
1180
  moduleIds: "deterministic",
@@ -1876,6 +1895,7 @@ var dev = async (options) => {
1876
1895
  swcPlugins: options.swcPlugins,
1877
1896
  define: options.define,
1878
1897
  moduleRules: options.moduleRules,
1898
+ reactMode: options.reactMode,
1879
1899
  htmlTemplate: resolvedHtmlTemplate
1880
1900
  });
1881
1901
  const devServer = new RspackDevServer({
@@ -1913,7 +1933,7 @@ var dev = async (options) => {
1913
1933
  `--base=${baseURL}`,
1914
1934
  ...options.swcPlugins ? [`--swcPlugins=${JSON.stringify(options.swcPlugins)}`] : [],
1915
1935
  ...options.define ? [`--define=${JSON.stringify(options.define)}`] : [],
1916
- ...options.moduleRules ? [`--moduleRules=${JSON.stringify(options.moduleRules)}`] : []
1936
+ ...options.moduleRules ? [`--moduleRules=${JSON.stringify(options.moduleRules, (_k, v) => v instanceof RegExp ? { __re: v.source, __flags: v.flags } : v)}`] : []
1917
1937
  ], { stdio: "pipe" });
1918
1938
  child.stdin?.end();
1919
1939
  const cleanupChild = () => {
@@ -2095,6 +2115,7 @@ var build = async (options) => {
2095
2115
  define: options.define,
2096
2116
  moduleRules: options.moduleRules,
2097
2117
  optimization: options.optimization,
2118
+ reactMode: options.reactMode,
2098
2119
  htmlTemplate: resolvedHtmlTemplate
2099
2120
  }),
2100
2121
  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.
package/dist/ssr-watch.js CHANGED
@@ -171,12 +171,11 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
171
171
  // Route all React imports to slim-react for SSR.
172
172
  react: slimReactIndex,
173
173
  "react/jsx-runtime": slimReactJsx,
174
- "react/jsx-dev-runtime": slimReactJsx,
175
- // Keep emotion on the project's node_modules (server-safe entry).
176
- "@emotion/react": path.resolve(process.cwd(), "node_modules", "@emotion", "react"),
177
- "@emotion/server": path.resolve(process.cwd(), "node_modules", "@emotion", "server"),
178
- "@emotion/cache": path.resolve(process.cwd(), "node_modules", "@emotion", "cache"),
179
- "@emotion/styled": path.resolve(process.cwd(), "node_modules", "@emotion", "styled")
174
+ "react/jsx-dev-runtime": slimReactJsx
175
+ // @emotion/* is bundled (not external) so that its `react` imports are
176
+ // resolved through the alias above to slim-react. If left external,
177
+ // emotion loads real React from node_modules and calls
178
+ // ReactSharedInternals.H.useContext which requires React's dispatcher.
180
179
  } : void 0;
181
180
  const externals = isServerBuild ? [
182
181
  // Node.js built-ins — must not be bundled; resolved by the runtime.
@@ -185,27 +184,47 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
185
184
  "node:os",
186
185
  "node:stream",
187
186
  "node:util",
188
- // react / react-dom are replaced by slim-react via alias above — not external.
189
- // emotion should be external on server builds to avoid client/browser code
190
- "@emotion/react",
191
- "@emotion/server",
192
- "@emotion/cache",
193
- "@emotion/styled"
187
+ // @emotion/server is only used outside component rendering (CSS extraction)
188
+ // and does not call React hooks, so it is safe to leave as external.
189
+ "@emotion/server"
194
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
+ }
195
209
  const extraPlugins = [];
196
- 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) {
197
215
  const DefinePlugin = rspack.DefinePlugin || rspack.plugins?.DefinePlugin;
198
216
  if (DefinePlugin) {
199
- extraPlugins.push(new DefinePlugin(opts.define));
200
- } else {
201
- extraPlugins.push({ name: "DefinePlugin", value: opts.define });
217
+ extraPlugins.push(new DefinePlugin(defineValues));
202
218
  }
203
219
  }
204
220
  const resolveConfig = {
205
221
  extensions: [".tsx", ".ts", ".js", ".jsx"],
206
222
  alias: resolveAliases,
207
223
  // for server builds prefer the package "main"/"module" fields and avoid "browser" so we don't pick browser-specific entrypoints
208
- mainFields: isServerBuild ? ["main", "module"] : ["browser", "module", "main"]
224
+ mainFields: isServerBuild ? ["main", "module"] : ["browser", "module", "main"],
225
+ // for server builds exclude the "browser" condition so packages with package.json
226
+ // "exports" conditions (e.g. @emotion/*) resolve their Node/CJS entry, not the browser build
227
+ ...isServerBuild ? { conditionNames: ["node", "require", "default"] } : {}
209
228
  };
210
229
  const optimization = !isServerBuild && !isDev ? {
211
230
  moduleIds: "deterministic",
@@ -355,7 +374,7 @@ var outFile = argv["outFile"] || "index.ssr.js";
355
374
  var base = argv["base"] || "";
356
375
  var swcPlugins = argv["swcPlugins"] ? JSON.parse(argv["swcPlugins"]) : void 0;
357
376
  var define = argv["define"] ? JSON.parse(argv["define"]) : void 0;
358
- 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;
359
378
  if (!entry) {
360
379
  console.error("ssr-watch: missing --entry argument");
361
380
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hadars",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
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), {
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 = (
@@ -218,33 +220,58 @@ const buildCompilerConfig = (
218
220
  react: slimReactIndex,
219
221
  'react/jsx-runtime': slimReactJsx,
220
222
  'react/jsx-dev-runtime': slimReactJsx,
221
- // Keep emotion on the project's node_modules (server-safe entry).
222
- '@emotion/react': path.resolve(process.cwd(), 'node_modules', '@emotion', 'react'),
223
- '@emotion/server': path.resolve(process.cwd(), 'node_modules', '@emotion', 'server'),
224
- '@emotion/cache': path.resolve(process.cwd(), 'node_modules', '@emotion', 'cache'),
225
- '@emotion/styled': path.resolve(process.cwd(), 'node_modules', '@emotion', 'styled'),
223
+ // @emotion/* is bundled (not external) so that its `react` imports are
224
+ // resolved through the alias above to slim-react. If left external,
225
+ // emotion loads real React from node_modules and calls
226
+ // ReactSharedInternals.H.useContext which requires React's dispatcher.
226
227
  } : undefined;
227
228
 
228
229
  const externals = isServerBuild ? [
229
230
  // Node.js built-ins — must not be bundled; resolved by the runtime.
230
231
  'node:fs', 'node:path', 'node:os', 'node:stream', 'node:util',
231
- // react / react-dom are replaced by slim-react via alias above — not external.
232
- // emotion should be external on server builds to avoid client/browser code
233
- '@emotion/react',
232
+ // @emotion/server is only used outside component rendering (CSS extraction)
233
+ // and does not call React hooks, so it is safe to leave as external.
234
234
  '@emotion/server',
235
- '@emotion/cache',
236
- '@emotion/styled',
237
235
  ] : undefined;
238
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
+
239
264
  const extraPlugins: any[] = [];
240
- if (opts.define && typeof opts.define === 'object') {
241
- // 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) {
242
272
  const DefinePlugin = (rspack as any).DefinePlugin || (rspack as any).plugins?.DefinePlugin;
243
273
  if (DefinePlugin) {
244
- extraPlugins.push(new DefinePlugin(opts.define));
245
- } else {
246
- // fallback: try to inject via plugin API name
247
- extraPlugins.push({ name: 'DefinePlugin', value: opts.define });
274
+ extraPlugins.push(new DefinePlugin(defineValues));
248
275
  }
249
276
  }
250
277
 
@@ -253,6 +280,9 @@ const buildCompilerConfig = (
253
280
  alias: resolveAliases,
254
281
  // for server builds prefer the package "main"/"module" fields and avoid "browser" so we don't pick browser-specific entrypoints
255
282
  mainFields: isServerBuild ? ['main', 'module'] : ['browser', 'module', 'main'],
283
+ // for server builds exclude the "browser" condition so packages with package.json
284
+ // "exports" conditions (e.g. @emotion/*) resolve their Node/CJS entry, not the browser build
285
+ ...(isServerBuild ? { conditionNames: ['node', 'require', 'default'] } : {}),
256
286
  };
257
287
 
258
288
  // Production client builds get vendor splitting and deterministic module IDs.