hadars 0.1.13 → 0.1.15

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
@@ -512,7 +512,7 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
512
512
  plugins: [
513
513
  new rspack.HtmlRspackPlugin({
514
514
  publicPath: base || "/",
515
- template: clientScriptPath,
515
+ template: opts.htmlTemplate ? pathMod2.resolve(process.cwd(), opts.htmlTemplate) : clientScriptPath,
516
516
  scriptLoading: "module",
517
517
  filename: "out.html",
518
518
  inject: "body"
@@ -551,7 +551,7 @@ var compileEntry = async (entry, opts) => {
551
551
  if (opts.watch) {
552
552
  await new Promise((resolve2, reject) => {
553
553
  let first = true;
554
- compiler.watch({}, (err, stats) => {
554
+ compiler.watch({ ignored: ["**/node_modules/**", "**/.hadars/**"] }, (err, stats) => {
555
555
  if (err) {
556
556
  if (first) {
557
557
  first = false;
@@ -561,13 +561,7 @@ var compileEntry = async (entry, opts) => {
561
561
  }
562
562
  return;
563
563
  }
564
- console.log(stats?.toString({
565
- colors: true,
566
- modules: true,
567
- children: true,
568
- chunks: true,
569
- chunkModules: true
570
- }));
564
+ console.log(stats?.toString({ colors: true }));
571
565
  if (first) {
572
566
  first = false;
573
567
  resolve2(stats);
@@ -790,6 +784,37 @@ import os from "node:os";
790
784
  import { spawn } from "node:child_process";
791
785
  import cluster from "node:cluster";
792
786
  var encoder = new TextEncoder();
787
+ async function processHtmlTemplate(templatePath) {
788
+ const html = await fs.readFile(templatePath, "utf-8");
789
+ const styleRegex = /<style([^>]*)>([\s\S]*?)<\/style>/gi;
790
+ const matches = [];
791
+ let m;
792
+ while ((m = styleRegex.exec(html)) !== null) {
793
+ matches.push({ full: m[0], attrs: m[1] ?? "", css: m[2] ?? "" });
794
+ }
795
+ if (matches.length === 0)
796
+ return templatePath;
797
+ const { default: postcss } = await import("postcss");
798
+ let plugins = [];
799
+ try {
800
+ const { default: loadConfig2 } = await import("postcss-load-config");
801
+ const config = await loadConfig2({}, process.cwd());
802
+ plugins = config.plugins ?? [];
803
+ } catch {
804
+ }
805
+ let processedHtml = html;
806
+ for (const { full, attrs, css } of matches) {
807
+ try {
808
+ const result = await postcss(plugins).process(css, { from: void 0 });
809
+ processedHtml = processedHtml.replace(full, `<style${attrs}>${result.css}</style>`);
810
+ } catch (err) {
811
+ console.warn("[hadars] PostCSS error processing <style> block in HTML template:", err);
812
+ }
813
+ }
814
+ const tmpPath = pathMod3.join(os.tmpdir(), `hadars-template-${Date.now()}.html`);
815
+ await fs.writeFile(tmpPath, processedHtml);
816
+ return tmpPath;
817
+ }
793
818
  var HEAD_MARKER = '<meta name="HADARS_HEAD">';
794
819
  var BODY_MARKER = '<meta name="HADARS_BODY">';
795
820
  var _renderToString = null;
@@ -1105,6 +1130,7 @@ var dev = async (options) => {
1105
1130
  const tmpFilePath = pathMod3.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
1106
1131
  await fs.writeFile(tmpFilePath, clientScript);
1107
1132
  let ssrBuildId = crypto.randomBytes(4).toString("hex");
1133
+ const resolvedHtmlTemplate = options.htmlTemplate ? await processHtmlTemplate(pathMod3.resolve(__dirname2, options.htmlTemplate)) : void 0;
1108
1134
  const clientCompiler = createClientCompiler(tmpFilePath, {
1109
1135
  target: "web",
1110
1136
  output: {
@@ -1114,7 +1140,8 @@ var dev = async (options) => {
1114
1140
  base: baseURL,
1115
1141
  mode: "development",
1116
1142
  swcPlugins: options.swcPlugins,
1117
- define: options.define
1143
+ define: options.define,
1144
+ htmlTemplate: resolvedHtmlTemplate
1118
1145
  });
1119
1146
  const devServer = new RspackDevServer({
1120
1147
  port: hmrPort,
@@ -1266,11 +1293,6 @@ var dev = async (options) => {
1266
1293
  if (staticRes)
1267
1294
  return staticRes;
1268
1295
  const projectStaticPath = pathMod3.resolve(process.cwd(), "static");
1269
- if (path2 === "/" || path2 === "") {
1270
- const indexRes = await tryServeFile(pathMod3.join(projectStaticPath, "index.html"));
1271
- if (indexRes)
1272
- return indexRes;
1273
- }
1274
1296
  const projectRes = await tryServeFile(pathMod3.join(projectStaticPath, path2));
1275
1297
  if (projectRes)
1276
1298
  return projectRes;
@@ -1318,6 +1340,7 @@ var build = async (options) => {
1318
1340
  }
1319
1341
  const tmpFilePath = pathMod3.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
1320
1342
  await fs.writeFile(tmpFilePath, clientScript);
1343
+ const resolvedHtmlTemplate = options.htmlTemplate ? await processHtmlTemplate(pathMod3.resolve(__dirname2, options.htmlTemplate)) : void 0;
1321
1344
  console.log("Building client and server bundles in parallel...");
1322
1345
  await Promise.all([
1323
1346
  compileEntry(tmpFilePath, {
@@ -1331,7 +1354,8 @@ var build = async (options) => {
1331
1354
  mode: "production",
1332
1355
  swcPlugins: options.swcPlugins,
1333
1356
  define: options.define,
1334
- optimization: options.optimization
1357
+ optimization: options.optimization,
1358
+ htmlTemplate: resolvedHtmlTemplate
1335
1359
  }),
1336
1360
  compileEntry(pathMod3.resolve(__dirname2, options.entry), {
1337
1361
  output: {
@@ -1401,11 +1425,6 @@ var run = async (options) => {
1401
1425
  const staticRes = await tryServeFile(pathMod3.join(__dirname2, StaticPath, path2));
1402
1426
  if (staticRes)
1403
1427
  return staticRes;
1404
- if (path2 === "/" || path2 === "") {
1405
- const indexRes = await tryServeFile(pathMod3.join(__dirname2, StaticPath, "index.html"));
1406
- if (indexRes)
1407
- return indexRes;
1408
- }
1409
1428
  const projectStaticPath = pathMod3.resolve(process.cwd(), "static");
1410
1429
  const projectRes = await tryServeFile(pathMod3.join(projectStaticPath, path2));
1411
1430
  if (projectRes)
package/dist/index.d.ts CHANGED
@@ -92,6 +92,24 @@ interface HadarsOptions {
92
92
  * Has no effect on the SSR bundle or dev mode.
93
93
  */
94
94
  optimization?: Record<string, unknown>;
95
+ /**
96
+ * Path to a custom HTML template file (relative to the project root).
97
+ * Replaces the built-in minimal template used to generate the HTML shell.
98
+ *
99
+ * The file must include two marker elements so hadars can inject the
100
+ * per-request head tags and the server-rendered body:
101
+ *
102
+ * ```html
103
+ * <meta name="HADARS_HEAD"> <!-- replaced with <title>, <meta>, <link>, <style> tags -->
104
+ * <meta name="HADARS_BODY"> <!-- replaced with the SSR-rendered React tree -->
105
+ * ```
106
+ *
107
+ * Any `<style>` blocks in the template are automatically processed through
108
+ * PostCSS (using the project's `postcss.config.js`) at build/dev startup time,
109
+ * so `@import "tailwindcss"` and other PostCSS directives work as expected.
110
+ * Note: inline styles are processed once at startup and are not live-reloaded.
111
+ */
112
+ htmlTemplate?: string;
95
113
  /**
96
114
  * SSR response cache for `run()` mode. Has no effect in `dev()` mode.
97
115
  *
package/dist/ssr-watch.js CHANGED
@@ -238,7 +238,7 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
238
238
  plugins: [
239
239
  new rspack.HtmlRspackPlugin({
240
240
  publicPath: base2 || "/",
241
- template: clientScriptPath,
241
+ template: opts.htmlTemplate ? pathMod.resolve(process.cwd(), opts.htmlTemplate) : clientScriptPath,
242
242
  scriptLoading: "module",
243
243
  filename: "out.html",
244
244
  inject: "body"
@@ -274,7 +274,7 @@ var compileEntry = async (entry2, opts) => {
274
274
  if (opts.watch) {
275
275
  await new Promise((resolve, reject) => {
276
276
  let first = true;
277
- compiler.watch({}, (err, stats) => {
277
+ compiler.watch({ ignored: ["**/node_modules/**", "**/.hadars/**"] }, (err, stats) => {
278
278
  if (err) {
279
279
  if (first) {
280
280
  first = false;
@@ -284,13 +284,7 @@ var compileEntry = async (entry2, opts) => {
284
284
  }
285
285
  return;
286
286
  }
287
- console.log(stats?.toString({
288
- colors: true,
289
- modules: true,
290
- children: true,
291
- chunks: true,
292
- chunkModules: true
293
- }));
287
+ console.log(stats?.toString({ colors: true }));
294
288
  if (first) {
295
289
  first = false;
296
290
  resolve(stats);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hadars",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Minimal SSR framework for React — rspack, HMR, TypeScript, Bun/Node/Deno",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -80,6 +80,7 @@
80
80
  "@mdx-js/loader": "^3.1.1",
81
81
  "@svgr/webpack": "^8.1.0",
82
82
  "postcss": "^8.5.8",
83
+ "postcss-load-config": "^6.0.1",
83
84
  "postcss-loader": "^8.2.1"
84
85
  },
85
86
  "license": "MIT",
package/src/build.ts CHANGED
@@ -19,6 +19,48 @@ import cluster from 'node:cluster';
19
19
  import type { HadarsEntryModule, HadarsOptions, HadarsProps } from "./types/hadars";
20
20
  const encoder = new TextEncoder();
21
21
 
22
+ /**
23
+ * Reads an HTML template, processes any `<style>` blocks through PostCSS
24
+ * (using the project's postcss.config.js), writes the result to a temp file,
25
+ * and returns the temp file path. If there are no `<style>` blocks the
26
+ * original path is returned unchanged.
27
+ */
28
+ async function processHtmlTemplate(templatePath: string): Promise<string> {
29
+ const html = await fs.readFile(templatePath, 'utf-8');
30
+
31
+ const styleRegex = /<style([^>]*)>([\s\S]*?)<\/style>/gi;
32
+ const matches: Array<{ full: string; attrs: string; css: string }> = [];
33
+ let m: RegExpExecArray | null;
34
+ while ((m = styleRegex.exec(html)) !== null) {
35
+ matches.push({ full: m[0]!, attrs: m[1] ?? '', css: m[2] ?? '' });
36
+ }
37
+ if (matches.length === 0) return templatePath;
38
+
39
+ const { default: postcss } = await import('postcss');
40
+ let plugins: any[] = [];
41
+ try {
42
+ const { default: loadConfig } = await import('postcss-load-config' as any);
43
+ const config = await loadConfig({}, process.cwd());
44
+ plugins = (config as any).plugins ?? [];
45
+ } catch {
46
+ // No postcss config found — process without plugins (passthrough)
47
+ }
48
+
49
+ let processedHtml = html;
50
+ for (const { full, attrs, css } of matches) {
51
+ try {
52
+ const result = await postcss(plugins).process(css, { from: undefined });
53
+ processedHtml = processedHtml.replace(full, `<style${attrs}>${result.css}</style>`);
54
+ } catch (err) {
55
+ console.warn('[hadars] PostCSS error processing <style> block in HTML template:', err);
56
+ }
57
+ }
58
+
59
+ const tmpPath = pathMod.join(os.tmpdir(), `hadars-template-${Date.now()}.html`);
60
+ await fs.writeFile(tmpPath, processedHtml);
61
+ return tmpPath;
62
+ }
63
+
22
64
  const HEAD_MARKER = '<meta name="HADARS_HEAD">';
23
65
  const BODY_MARKER = '<meta name="HADARS_BODY">';
24
66
 
@@ -445,6 +487,11 @@ export const dev = async (options: HadarsRuntimeOptions) => {
445
487
  // SSR live-reload id to force re-import
446
488
  let ssrBuildId = crypto.randomBytes(4).toString('hex');
447
489
 
490
+ // Pre-process the HTML template's <style> blocks through PostCSS (e.g. Tailwind).
491
+ const resolvedHtmlTemplate = options.htmlTemplate
492
+ ? await processHtmlTemplate(pathMod.resolve(__dirname, options.htmlTemplate))
493
+ : undefined;
494
+
448
495
  // Start rspack-dev-server for the client bundle. It provides true React
449
496
  // Fast Refresh HMR: the browser's HMR runtime connects directly to the
450
497
  // dev server's WebSocket on hmrPort and receives module-level patches
@@ -460,6 +507,7 @@ export const dev = async (options: HadarsRuntimeOptions) => {
460
507
  mode: 'development',
461
508
  swcPlugins: options.swcPlugins,
462
509
  define: options.define,
510
+ htmlTemplate: resolvedHtmlTemplate,
463
511
  });
464
512
 
465
513
  const devServer = new RspackDevServer({
@@ -609,12 +657,8 @@ export const dev = async (options: HadarsRuntimeOptions) => {
609
657
  const staticRes = await tryServeFile(pathMod.join(__dirname, StaticPath, path));
610
658
  if (staticRes) return staticRes;
611
659
 
612
- // project-level static/ directory
660
+ // project-level static/ directory (explicit paths only — never intercept root)
613
661
  const projectStaticPath = pathMod.resolve(process.cwd(), 'static');
614
- if (path === '/' || path === '') {
615
- const indexRes = await tryServeFile(pathMod.join(projectStaticPath, 'index.html'));
616
- if (indexRes) return indexRes;
617
- }
618
662
  const projectRes = await tryServeFile(pathMod.join(projectStaticPath, path));
619
663
  if (projectRes) return projectRes;
620
664
 
@@ -675,6 +719,11 @@ export const build = async (options: HadarsRuntimeOptions) => {
675
719
  const tmpFilePath = pathMod.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
676
720
  await fs.writeFile(tmpFilePath, clientScript);
677
721
 
722
+ // Pre-process the HTML template's <style> blocks through PostCSS (e.g. Tailwind).
723
+ const resolvedHtmlTemplate = options.htmlTemplate
724
+ ? await processHtmlTemplate(pathMod.resolve(__dirname, options.htmlTemplate))
725
+ : undefined;
726
+
678
727
  // Compile client and SSR bundles in parallel — they write to different
679
728
  // output directories and use different entry files, so they are fully
680
729
  // independent and safe to run concurrently.
@@ -692,6 +741,7 @@ export const build = async (options: HadarsRuntimeOptions) => {
692
741
  swcPlugins: options.swcPlugins,
693
742
  define: options.define,
694
743
  optimization: options.optimization,
744
+ htmlTemplate: resolvedHtmlTemplate,
695
745
  }),
696
746
  compileEntry(pathMod.resolve(__dirname, options.entry), {
697
747
  output: {
@@ -775,12 +825,7 @@ export const run = async (options: HadarsRuntimeOptions) => {
775
825
  const staticRes = await tryServeFile(pathMod.join(__dirname, StaticPath, path));
776
826
  if (staticRes) return staticRes;
777
827
 
778
- if (path === '/' || path === '') {
779
- const indexRes = await tryServeFile(pathMod.join(__dirname, StaticPath, 'index.html'));
780
- if (indexRes) return indexRes;
781
- }
782
-
783
- // project-level static/ directory
828
+ // project-level static/ directory (explicit paths only never intercept root)
784
829
  const projectStaticPath = pathMod.resolve(process.cwd(), 'static');
785
830
  const projectRes = await tryServeFile(pathMod.join(projectStaticPath, path));
786
831
  if (projectRes) return projectRes;
@@ -93,6 +93,24 @@ export interface HadarsOptions {
93
93
  * Has no effect on the SSR bundle or dev mode.
94
94
  */
95
95
  optimization?: Record<string, unknown>;
96
+ /**
97
+ * Path to a custom HTML template file (relative to the project root).
98
+ * Replaces the built-in minimal template used to generate the HTML shell.
99
+ *
100
+ * The file must include two marker elements so hadars can inject the
101
+ * per-request head tags and the server-rendered body:
102
+ *
103
+ * ```html
104
+ * <meta name="HADARS_HEAD"> <!-- replaced with <title>, <meta>, <link>, <style> tags -->
105
+ * <meta name="HADARS_BODY"> <!-- replaced with the SSR-rendered React tree -->
106
+ * ```
107
+ *
108
+ * Any `<style>` blocks in the template are automatically processed through
109
+ * PostCSS (using the project's `postcss.config.js`) at build/dev startup time,
110
+ * so `@import "tailwindcss"` and other PostCSS directives work as expected.
111
+ * Note: inline styles are processed once at startup and are not live-reloaded.
112
+ */
113
+ htmlTemplate?: string;
96
114
  /**
97
115
  * SSR response cache for `run()` mode. Has no effect in `dev()` mode.
98
116
  *
@@ -135,6 +135,8 @@ interface EntryOptions {
135
135
  mode: "development" | "production",
136
136
  // optional swc plugins to pass to swc-loader
137
137
  swcPlugins?: SwcPluginList,
138
+ // optional path to a custom HTML template (resolved relative to cwd)
139
+ htmlTemplate?: string,
138
140
  // optional compile-time defines (e.g. { 'process.env.NODE_ENV': '"development"' })
139
141
  define?: Record<string, string>;
140
142
  base?: string;
@@ -291,7 +293,9 @@ const buildCompilerConfig = (
291
293
  plugins: [
292
294
  new rspack.HtmlRspackPlugin({
293
295
  publicPath: base || '/',
294
- template: clientScriptPath,
296
+ template: opts.htmlTemplate
297
+ ? pathMod.resolve(process.cwd(), opts.htmlTemplate)
298
+ : clientScriptPath,
295
299
  scriptLoading: 'module',
296
300
  filename: 'out.html',
297
301
  inject: 'body',
@@ -341,20 +345,16 @@ export const compileEntry = async (entry: string, opts: EntryOptions & { watch?:
341
345
  if (opts.watch) {
342
346
  await new Promise((resolve, reject) => {
343
347
  let first = true;
344
- compiler.watch({}, (err: any, stats: any) => {
348
+ // Pass ignored patterns directly — compiler.watch(watchOptions) replaces
349
+ // the config-level watchOptions, so we must repeat them here.
350
+ compiler.watch({ ignored: ['**/node_modules/**', '**/.hadars/**'] }, (err: any, stats: any) => {
345
351
  if (err) {
346
352
  if (first) { first = false; reject(err); }
347
353
  else { console.error('rspack watch error', err); }
348
354
  return;
349
355
  }
350
356
 
351
- console.log(stats?.toString({
352
- colors: true,
353
- modules: true,
354
- children: true,
355
- chunks: true,
356
- chunkModules: true,
357
- }));
357
+ console.log(stats?.toString({ colors: true }));
358
358
 
359
359
  if (first) {
360
360
  first = false;