hadars 0.1.14 → 0.1.16

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"
@@ -784,6 +784,37 @@ import os from "node:os";
784
784
  import { spawn } from "node:child_process";
785
785
  import cluster from "node:cluster";
786
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: templatePath });
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
+ }
787
818
  var HEAD_MARKER = '<meta name="HADARS_HEAD">';
788
819
  var BODY_MARKER = '<meta name="HADARS_BODY">';
789
820
  var _renderToString = null;
@@ -1099,6 +1130,7 @@ var dev = async (options) => {
1099
1130
  const tmpFilePath = pathMod3.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
1100
1131
  await fs.writeFile(tmpFilePath, clientScript);
1101
1132
  let ssrBuildId = crypto.randomBytes(4).toString("hex");
1133
+ const resolvedHtmlTemplate = options.htmlTemplate ? await processHtmlTemplate(pathMod3.resolve(__dirname2, options.htmlTemplate)) : void 0;
1102
1134
  const clientCompiler = createClientCompiler(tmpFilePath, {
1103
1135
  target: "web",
1104
1136
  output: {
@@ -1108,7 +1140,8 @@ var dev = async (options) => {
1108
1140
  base: baseURL,
1109
1141
  mode: "development",
1110
1142
  swcPlugins: options.swcPlugins,
1111
- define: options.define
1143
+ define: options.define,
1144
+ htmlTemplate: resolvedHtmlTemplate
1112
1145
  });
1113
1146
  const devServer = new RspackDevServer({
1114
1147
  port: hmrPort,
@@ -1260,11 +1293,6 @@ var dev = async (options) => {
1260
1293
  if (staticRes)
1261
1294
  return staticRes;
1262
1295
  const projectStaticPath = pathMod3.resolve(process.cwd(), "static");
1263
- if (path2 === "/" || path2 === "") {
1264
- const indexRes = await tryServeFile(pathMod3.join(projectStaticPath, "index.html"));
1265
- if (indexRes)
1266
- return indexRes;
1267
- }
1268
1296
  const projectRes = await tryServeFile(pathMod3.join(projectStaticPath, path2));
1269
1297
  if (projectRes)
1270
1298
  return projectRes;
@@ -1312,6 +1340,7 @@ var build = async (options) => {
1312
1340
  }
1313
1341
  const tmpFilePath = pathMod3.join(os.tmpdir(), `hadars-client-${Date.now()}.tsx`);
1314
1342
  await fs.writeFile(tmpFilePath, clientScript);
1343
+ const resolvedHtmlTemplate = options.htmlTemplate ? await processHtmlTemplate(pathMod3.resolve(__dirname2, options.htmlTemplate)) : void 0;
1315
1344
  console.log("Building client and server bundles in parallel...");
1316
1345
  await Promise.all([
1317
1346
  compileEntry(tmpFilePath, {
@@ -1325,7 +1354,8 @@ var build = async (options) => {
1325
1354
  mode: "production",
1326
1355
  swcPlugins: options.swcPlugins,
1327
1356
  define: options.define,
1328
- optimization: options.optimization
1357
+ optimization: options.optimization,
1358
+ htmlTemplate: resolvedHtmlTemplate
1329
1359
  }),
1330
1360
  compileEntry(pathMod3.resolve(__dirname2, options.entry), {
1331
1361
  output: {
@@ -1395,11 +1425,6 @@ var run = async (options) => {
1395
1425
  const staticRes = await tryServeFile(pathMod3.join(__dirname2, StaticPath, path2));
1396
1426
  if (staticRes)
1397
1427
  return staticRes;
1398
- if (path2 === "/" || path2 === "") {
1399
- const indexRes = await tryServeFile(pathMod3.join(__dirname2, StaticPath, "index.html"));
1400
- if (indexRes)
1401
- return indexRes;
1402
- }
1403
1428
  const projectStaticPath = pathMod3.resolve(process.cwd(), "static");
1404
1429
  const projectRes = await tryServeFile(pathMod3.join(projectStaticPath, path2));
1405
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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hadars",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
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: templatePath });
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',