hadars 1.0.7 → 1.0.10

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.
@@ -137,8 +137,8 @@ var ATTR = {
137
137
  fetchPriority: "fetchpriority",
138
138
  hrefLang: "hreflang"
139
139
  };
140
- function renderHeadTag(tag, id, opts, selfClose = false) {
141
- let attrs = ` id="${escAttr(id)}"`;
140
+ function renderHeadTag(tag, _id, opts, selfClose = false) {
141
+ let attrs = "";
142
142
  let inner = "";
143
143
  for (const [k, v] of Object.entries(opts)) {
144
144
  if (k === "key" || k === "children") continue;
@@ -318,6 +318,7 @@ async function buildCacheEntry(res, ttl) {
318
318
  }
319
319
  });
320
320
  headers.push(["content-encoding", "gzip"]);
321
+ headers.push(["vary", "accept-encoding"]);
321
322
  return { body, status: res.status, headers, expiresAt: ttl != null ? Date.now() + ttl : null };
322
323
  }
323
324
  async function serveFromEntry(entry, req) {
package/dist/cli.js CHANGED
@@ -1,12 +1,103 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
+ var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, {
11
+ get: all[name],
12
+ enumerable: true,
13
+ configurable: true,
14
+ set: __exportSetter.bind(all, name)
15
+ });
16
+ };
17
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
3
18
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
19
 
20
+ // src/utils/imageOptimizer.ts
21
+ var exports_imageOptimizer = {};
22
+ __export(exports_imageOptimizer, {
23
+ optimizeImages: () => optimizeImages
24
+ });
25
+ import fs from "node:fs/promises";
26
+ import { existsSync as existsSync2 } from "node:fs";
27
+ import path2 from "node:path";
28
+ async function optimizeImages(srcDir, destDir, opts = {}) {
29
+ const widths = opts.widths ?? [640, 1280, 1920];
30
+ const formats = opts.formats ?? ["webp"];
31
+ const quality = opts.quality ?? 80;
32
+ let sharp;
33
+ try {
34
+ const mod = await import("sharp");
35
+ sharp = mod.default ?? mod;
36
+ } catch {
37
+ console.warn('[hadars] `images` config requires "sharp" — run: npm install sharp');
38
+ return;
39
+ }
40
+ if (!existsSync2(srcDir))
41
+ return;
42
+ const outBase = path2.join(destDir, "_images");
43
+ await fs.mkdir(outBase, { recursive: true });
44
+ await processDir(srcDir, srcDir, outBase, widths, formats, quality, sharp);
45
+ console.log(`[hadars] Image variants written to ${outBase}`);
46
+ }
47
+ async function processDir(baseDir, dir, outBase, widths, formats, quality, sharp) {
48
+ let entries;
49
+ try {
50
+ entries = await fs.readdir(dir, { withFileTypes: true });
51
+ } catch {
52
+ return;
53
+ }
54
+ await Promise.all(entries.map(async (entry) => {
55
+ const fullPath = path2.join(dir, entry.name);
56
+ if (entry.isDirectory()) {
57
+ await processDir(baseDir, fullPath, outBase, widths, formats, quality, sharp);
58
+ return;
59
+ }
60
+ if (!entry.isFile())
61
+ return;
62
+ const ext = path2.extname(entry.name).toLowerCase();
63
+ if (!SUPPORTED_EXTS.has(ext))
64
+ return;
65
+ const rel = path2.relative(baseDir, fullPath);
66
+ const relNoExt = rel.slice(0, -ext.length);
67
+ const outDir = path2.join(outBase, path2.dirname(rel));
68
+ await fs.mkdir(outDir, { recursive: true });
69
+ const baseName = path2.basename(relNoExt);
70
+ let srcMtime = 0;
71
+ try {
72
+ srcMtime = (await fs.stat(fullPath)).mtimeMs;
73
+ } catch {
74
+ return;
75
+ }
76
+ await Promise.all(widths.flatMap((width) => formats.map(async (fmt) => {
77
+ const outFile = path2.join(outDir, `${baseName}-${width}.${fmt}`);
78
+ try {
79
+ const outMtime = (await fs.stat(outFile)).mtimeMs;
80
+ if (outMtime >= srcMtime)
81
+ return;
82
+ } catch {}
83
+ try {
84
+ await sharp(fullPath).resize({ width, withoutEnlargement: true })[fmt]({ quality }).toFile(outFile);
85
+ } catch (err) {
86
+ console.warn(`[hadars] Could not generate ${rel} → ${width}px.${fmt}:`, err);
87
+ }
88
+ })));
89
+ }));
90
+ }
91
+ var SUPPORTED_EXTS;
92
+ var init_imageOptimizer = __esm(() => {
93
+ SUPPORTED_EXTS = new Set([".jpg", ".jpeg", ".png", ".tiff", ".bmp", ".webp", ".avif"]);
94
+ });
95
+
5
96
  // cli.ts
6
97
  import { spawn as spawn2 } from "node:child_process";
7
98
 
8
99
  // cli-lib.ts
9
- import { existsSync as existsSync3 } from "node:fs";
100
+ import { existsSync as existsSync4 } from "node:fs";
10
101
  import { mkdir as mkdir2, writeFile as writeFile2, unlink, readFile as readFile2 } from "node:fs/promises";
11
102
  import { resolve, join as join2, dirname as dirname2 } from "node:path";
12
103
  import { fileURLToPath as fileURLToPath3, pathToFileURL as pathToFileURL2 } from "node:url";
@@ -1182,8 +1273,8 @@ var ATTR = {
1182
1273
  fetchPriority: "fetchpriority",
1183
1274
  hrefLang: "hreflang"
1184
1275
  };
1185
- function renderHeadTag(tag, id, opts, selfClose = false) {
1186
- let attrs = ` id="${escAttr(id)}"`;
1276
+ function renderHeadTag(tag, _id, opts, selfClose = false) {
1277
+ let attrs = "";
1187
1278
  let inner = "";
1188
1279
  for (const [k, v] of Object.entries(opts)) {
1189
1280
  if (k === "key" || k === "children")
@@ -1772,11 +1863,12 @@ async function tryServeFile(filePath) {
1772
1863
  }
1773
1864
  }
1774
1865
  var _notFound = new Set;
1866
+ var _NOT_FOUND_MAX = 50000;
1775
1867
  async function tryServeFileCached(filePath) {
1776
1868
  if (_notFound.has(filePath))
1777
1869
  return null;
1778
1870
  const res = await tryServeFile(filePath);
1779
- if (!res)
1871
+ if (!res && _notFound.size < _NOT_FOUND_MAX)
1780
1872
  _notFound.add(filePath);
1781
1873
  return res;
1782
1874
  }
@@ -1786,8 +1878,8 @@ import { RspackDevServer } from "@rspack/dev-server";
1786
1878
  import pathMod2 from "node:path";
1787
1879
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
1788
1880
  import crypto from "node:crypto";
1789
- import fs from "node:fs/promises";
1790
- import { existsSync as existsSync2 } from "node:fs";
1881
+ import fs2 from "node:fs/promises";
1882
+ import { existsSync as existsSync3 } from "node:fs";
1791
1883
  import os from "node:os";
1792
1884
  import { spawn } from "node:child_process";
1793
1885
  import cluster from "node:cluster";
@@ -1812,6 +1904,7 @@ function buildSsrResponse(head, status, getAppBody, finalize, getPrecontentHtml)
1812
1904
  controller.enqueue(encoder.encode(`<script id="hadars" type="application/json">${scriptContent}</script>` + postContent));
1813
1905
  controller.close();
1814
1906
  } catch (err) {
1907
+ console.error("[hadars] SSR render error:", err);
1815
1908
  controller.error(err);
1816
1909
  }
1817
1910
  }
@@ -1895,6 +1988,7 @@ async function buildCacheEntry(res, ttl) {
1895
1988
  }
1896
1989
  });
1897
1990
  headers.push(["content-encoding", "gzip"]);
1991
+ headers.push(["vary", "accept-encoding"]);
1898
1992
  return { body, status: res.status, headers, expiresAt: ttl != null ? Date.now() + ttl : null };
1899
1993
  }
1900
1994
  async function serveFromEntry(entry, req) {
@@ -1956,13 +2050,12 @@ class NodeStore {
1956
2050
  if (!node.internal?.type)
1957
2051
  throw new Error("[hadars] createNode: node.internal.type must be a non-empty string");
1958
2052
  this.byId.set(node.id, node);
1959
- const list = this.byType.get(node.internal.type) ?? [];
1960
- const idx = list.findIndex((n) => n.id === node.id);
1961
- if (idx >= 0)
1962
- list[idx] = node;
1963
- else
1964
- list.push(node);
1965
- this.byType.set(node.internal.type, list);
2053
+ let typeMap = this.byType.get(node.internal.type);
2054
+ if (!typeMap) {
2055
+ typeMap = new Map;
2056
+ this.byType.set(node.internal.type, typeMap);
2057
+ }
2058
+ typeMap.set(node.id, node);
1966
2059
  }
1967
2060
  getNode(id) {
1968
2061
  return this.byId.get(id);
@@ -1971,7 +2064,8 @@ class NodeStore {
1971
2064
  return Array.from(this.byId.values());
1972
2065
  }
1973
2066
  getNodesByType(type) {
1974
- return this.byType.get(type) ?? [];
2067
+ const typeMap = this.byType.get(type);
2068
+ return typeMap ? Array.from(typeMap.values()) : [];
1975
2069
  }
1976
2070
  getTypes() {
1977
2071
  return Array.from(this.byType.keys());
@@ -2360,7 +2454,7 @@ function createGraphiqlHandler(executor) {
2360
2454
 
2361
2455
  // src/build.ts
2362
2456
  async function processHtmlTemplate(templatePath) {
2363
- const html = await fs.readFile(templatePath, "utf-8");
2457
+ const html = await fs2.readFile(templatePath, "utf-8");
2364
2458
  const styleRegex = /<style([^>]*)>([\s\S]*?)<\/style>/gi;
2365
2459
  const matches = [];
2366
2460
  let m;
@@ -2373,7 +2467,7 @@ async function processHtmlTemplate(templatePath) {
2373
2467
  const sourceHash = crypto.createHash("md5").update(html).digest("hex").slice(0, 8);
2374
2468
  const cachedPath = pathMod2.join(HADARS_TMP_DIR, `template-${sourceHash}.html`);
2375
2469
  try {
2376
- await fs.access(cachedPath);
2470
+ await fs2.access(cachedPath);
2377
2471
  return cachedPath;
2378
2472
  } catch {}
2379
2473
  const { default: postcss } = await import("postcss");
@@ -2392,7 +2486,7 @@ async function processHtmlTemplate(templatePath) {
2392
2486
  console.warn("[hadars] PostCSS error processing <style> block in HTML template:", err);
2393
2487
  }
2394
2488
  }
2395
- await fs.writeFile(cachedPath, processedHtml);
2489
+ await fs2.writeFile(cachedPath, processedHtml);
2396
2490
  return cachedPath;
2397
2491
  }
2398
2492
 
@@ -2425,15 +2519,18 @@ class RenderWorkerPool {
2425
2519
  const w = new this._Worker(this._workerPath, { workerData: { ssrBundlePath: this._ssrBundlePath } });
2426
2520
  this.workerPending.set(w, new Set);
2427
2521
  w.on("message", (msg) => {
2428
- const { id, html, headHtml, status, error } = msg;
2522
+ const { id, html, headHtml, status, error, stack } = msg;
2429
2523
  const p = this.pending.get(id);
2430
2524
  if (!p)
2431
2525
  return;
2432
2526
  this.pending.delete(id);
2433
2527
  this.workerPending.get(w)?.delete(id);
2434
- if (error)
2435
- p.reject(new Error(error));
2436
- else
2528
+ if (error) {
2529
+ const e = new Error(error);
2530
+ if (stack)
2531
+ e.stack = stack;
2532
+ p.reject(e);
2533
+ } else
2437
2534
  p.resolve({ html, headHtml, status });
2438
2535
  });
2439
2536
  w.on("error", (err) => {
@@ -2525,13 +2622,13 @@ var getSuffix = (mode) => mode === "development" ? `?v=${Date.now()}` : "";
2525
2622
  var HadarsFolder = "./.hadars";
2526
2623
  var StaticPath = `${HadarsFolder}/static`;
2527
2624
  var HADARS_TMP_DIR = pathMod2.join(os.tmpdir(), "hadars");
2528
- var ensureHadarsTmpDir = () => fs.mkdir(HADARS_TMP_DIR, { recursive: true });
2625
+ var ensureHadarsTmpDir = () => fs2.mkdir(HADARS_TMP_DIR, { recursive: true });
2529
2626
  var readReactMajor = async () => {
2530
2627
  let dir = process.cwd();
2531
2628
  while (true) {
2532
2629
  try {
2533
2630
  const pkgPath = pathMod2.join(dir, "node_modules", "react", "package.json");
2534
- const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
2631
+ const pkg = JSON.parse(await fs2.readFile(pkgPath, "utf-8"));
2535
2632
  return parseInt(pkg.version.split(".")[0], 10);
2536
2633
  } catch {}
2537
2634
  const parent = pathMod2.dirname(dir);
@@ -2551,13 +2648,13 @@ var validateOptions = (options) => {
2551
2648
  var resolveWorkerCmd = (packageDir2) => {
2552
2649
  const tsPath = pathMod2.resolve(packageDir2, "ssr-watch.ts");
2553
2650
  const jsPath = pathMod2.resolve(packageDir2, "ssr-watch.js");
2554
- if (isBun && existsSync2(tsPath)) {
2651
+ if (isBun && existsSync3(tsPath)) {
2555
2652
  return ["bun", tsPath];
2556
2653
  }
2557
- if (isDeno && existsSync2(tsPath)) {
2654
+ if (isDeno && existsSync3(tsPath)) {
2558
2655
  return ["deno", "run", "--allow-all", tsPath];
2559
2656
  }
2560
- if (existsSync2(tsPath)) {
2657
+ if (existsSync3(tsPath)) {
2561
2658
  const allArgs = [...process.execArgv, process.argv[1] ?? ""];
2562
2659
  const hasTsx = allArgs.some((a) => a.includes("tsx"));
2563
2660
  const hasTsNode = allArgs.some((a) => a.includes("ts-node"));
@@ -2566,7 +2663,7 @@ var resolveWorkerCmd = (packageDir2) => {
2566
2663
  if (hasTsNode)
2567
2664
  return ["ts-node", tsPath];
2568
2665
  }
2569
- if (existsSync2(jsPath)) {
2666
+ if (existsSync3(jsPath)) {
2570
2667
  return ["node", jsPath];
2571
2668
  }
2572
2669
  throw new Error(`[hadars] SSR worker not found. Expected:
@@ -2575,7 +2672,7 @@ Run "npm run build:cli" to compile it, or launch hadars via a TypeScript runner:
2575
2672
  npx tsx cli.ts dev`);
2576
2673
  };
2577
2674
  var dev = async (options) => {
2578
- await fs.rm(HadarsFolder, { recursive: true, force: true });
2675
+ await fs2.rm(HadarsFolder, { recursive: true, force: true });
2579
2676
  let { port = 9090, baseURL = "" } = options;
2580
2677
  console.log(`Starting Hadars on port ${port}`);
2581
2678
  validateOptions(options);
@@ -2606,14 +2703,14 @@ var dev = async (options) => {
2606
2703
  const clientScriptPath2 = pathMod2.resolve(packageDir2, "utils", "clientScript.tsx");
2607
2704
  let clientScript = "";
2608
2705
  try {
2609
- clientScript = (await fs.readFile(clientScriptPath2, "utf-8")).replace("$_MOD_PATH$", entry + getSuffix(options.mode));
2706
+ clientScript = (await fs2.readFile(clientScriptPath2, "utf-8")).replace("$_MOD_PATH$", entry + getSuffix(options.mode));
2610
2707
  } catch (err) {
2611
2708
  console.error("Failed to read client script from package dist, falling back to src", err);
2612
2709
  throw err;
2613
2710
  }
2614
2711
  await ensureHadarsTmpDir();
2615
2712
  const tmpFilePath = pathMod2.join(HADARS_TMP_DIR, `client-${Date.now()}.tsx`);
2616
- await fs.writeFile(tmpFilePath, clientScript);
2713
+ await fs2.writeFile(tmpFilePath, clientScript);
2617
2714
  let ssrBuildId = crypto.randomBytes(4).toString("hex");
2618
2715
  let cachedSsrModule = null;
2619
2716
  let cachedSsrBuildId = "";
@@ -2727,7 +2824,17 @@ var dev = async (options) => {
2727
2824
  }
2728
2825
  })();
2729
2826
  const readyPromise = Promise.all([clientBuildDone, ssrBuildDone]);
2730
- readyPromise.then(() => {
2827
+ readyPromise.then(async () => {
2828
+ if (options.images) {
2829
+ try {
2830
+ const { optimizeImages: optimizeImages2 } = await Promise.resolve().then(() => (init_imageOptimizer(), exports_imageOptimizer));
2831
+ const projectStaticDir = pathMod2.resolve(__dirname3, "static");
2832
+ const hadarStaticDir = pathMod2.resolve(__dirname3, StaticPath);
2833
+ await optimizeImages2(projectStaticDir, hadarStaticDir, options.images);
2834
+ } catch (err) {
2835
+ console.warn("[hadars] Image optimization failed in dev mode:", err);
2836
+ }
2837
+ }
2731
2838
  if (stdoutReader) {
2732
2839
  const reader = stdoutReader;
2733
2840
  (async () => {
@@ -2762,7 +2869,7 @@ var dev = async (options) => {
2762
2869
  }
2763
2870
  } catch (e) {}
2764
2871
  })();
2765
- const getPrecontentHtml = makePrecontentHtmlGetter(readyPromise.then(() => fs.readFile(pathMod2.join(__dirname3, StaticPath, "out.html"), "utf-8")));
2872
+ const getPrecontentHtml = makePrecontentHtmlGetter(readyPromise.then(() => fs2.readFile(pathMod2.join(__dirname3, StaticPath, "out.html"), "utf-8")));
2766
2873
  const projectStaticPath = pathMod2.resolve(process.cwd(), "static");
2767
2874
  await serve(port, async (req, ctx) => {
2768
2875
  await readyPromise;
@@ -2783,11 +2890,11 @@ var dev = async (options) => {
2783
2890
  if (proxied)
2784
2891
  return proxied;
2785
2892
  const url = new URL(request.url);
2786
- const path2 = url.pathname;
2787
- const staticRes = await tryServeFileCached(pathMod2.join(__dirname3, StaticPath, path2));
2893
+ const path3 = url.pathname;
2894
+ const staticRes = await tryServeFileCached(pathMod2.join(__dirname3, StaticPath, path3));
2788
2895
  if (staticRes)
2789
2896
  return staticRes;
2790
- const projectRes = await tryServeFileCached(pathMod2.join(projectStaticPath, path2));
2897
+ const projectRes = await tryServeFileCached(pathMod2.join(projectStaticPath, path3));
2791
2898
  if (projectRes)
2792
2899
  return projectRes;
2793
2900
  const ssrComponentPath = pathMod2.join(__dirname3, HadarsFolder, SSR_FILENAME);
@@ -2842,14 +2949,14 @@ var build = async (options) => {
2842
2949
  const clientScriptPath2 = pathMod2.resolve(packageDir2, "utils", "clientScript.js");
2843
2950
  let clientScript = "";
2844
2951
  try {
2845
- clientScript = (await fs.readFile(clientScriptPath2, "utf-8")).replace("$_MOD_PATH$", entry + getSuffix(options.mode));
2952
+ clientScript = (await fs2.readFile(clientScriptPath2, "utf-8")).replace("$_MOD_PATH$", entry + getSuffix(options.mode));
2846
2953
  } catch (err) {
2847
2954
  const srcClientPath = pathMod2.resolve(packageDir2, "utils", "clientScript.tsx");
2848
- clientScript = (await fs.readFile(srcClientPath, "utf-8")).replace("$_MOD_PATH$", entry + `?v=${Date.now()}`);
2955
+ clientScript = (await fs2.readFile(srcClientPath, "utf-8")).replace("$_MOD_PATH$", entry + `?v=${Date.now()}`);
2849
2956
  }
2850
2957
  await ensureHadarsTmpDir();
2851
2958
  const tmpFilePath = pathMod2.join(HADARS_TMP_DIR, `client-${Date.now()}.tsx`);
2852
- await fs.writeFile(tmpFilePath, clientScript);
2959
+ await fs2.writeFile(tmpFilePath, clientScript);
2853
2960
  const resolvedHtmlTemplate = options.htmlTemplate ? await processHtmlTemplate(pathMod2.resolve(__dirname3, options.htmlTemplate)) : undefined;
2854
2961
  const reactMajor = await readReactMajor();
2855
2962
  console.log("Building client and server bundles in parallel...");
@@ -2889,7 +2996,13 @@ var build = async (options) => {
2889
2996
  postcssPlugins: options.postcssPlugins
2890
2997
  })
2891
2998
  ]);
2892
- await fs.rm(tmpFilePath);
2999
+ await fs2.rm(tmpFilePath);
3000
+ if (options.images) {
3001
+ const { optimizeImages: optimizeImages2 } = await Promise.resolve().then(() => (init_imageOptimizer(), exports_imageOptimizer));
3002
+ const projectStaticDir = pathMod2.resolve(__dirname3, "static");
3003
+ const hadarStaticDir = pathMod2.resolve(__dirname3, StaticPath);
3004
+ await optimizeImages2(projectStaticDir, hadarStaticDir, options.images);
3005
+ }
2893
3006
  console.log("Build complete.");
2894
3007
  };
2895
3008
  var run = async (options) => {
@@ -2916,12 +3029,12 @@ var run = async (options) => {
2916
3029
  const packageDir2 = pathMod2.dirname(fileURLToPath2(import.meta.url));
2917
3030
  const workerJs = pathMod2.resolve(packageDir2, "ssr-render-worker.js");
2918
3031
  const workerTs = pathMod2.resolve(packageDir2, "ssr-render-worker.ts");
2919
- const workerFile = existsSync2(workerJs) ? workerJs : workerTs;
3032
+ const workerFile = existsSync3(workerJs) ? workerJs : workerTs;
2920
3033
  const ssrBundlePath = pathMod2.resolve(__dirname3, HadarsFolder, SSR_FILENAME);
2921
3034
  renderPool = new RenderWorkerPool(workerFile, workers, ssrBundlePath);
2922
3035
  console.log(`[hadars] SSR render pool: ${workers} worker threads`);
2923
3036
  }
2924
- const getPrecontentHtml = makePrecontentHtmlGetter(fs.readFile(pathMod2.join(__dirname3, StaticPath, "out.html"), "utf-8"));
3037
+ const getPrecontentHtml = makePrecontentHtmlGetter(fs2.readFile(pathMod2.join(__dirname3, StaticPath, "out.html"), "utf-8"));
2925
3038
  const projectStaticPath = pathMod2.resolve(process.cwd(), "static");
2926
3039
  const componentPath = pathToFileURL(pathMod2.resolve(__dirname3, HadarsFolder, SSR_FILENAME)).href;
2927
3040
  const ssrModulePromise = import(componentPath);
@@ -2938,14 +3051,14 @@ var run = async (options) => {
2938
3051
  if (proxied)
2939
3052
  return proxied;
2940
3053
  const url = new URL(request.url);
2941
- const path2 = url.pathname;
2942
- const staticRes = await tryServeFileCached(pathMod2.join(__dirname3, StaticPath, path2));
3054
+ const path3 = url.pathname;
3055
+ const staticRes = await tryServeFileCached(pathMod2.join(__dirname3, StaticPath, path3));
2943
3056
  if (staticRes)
2944
3057
  return staticRes;
2945
- const projectRes = await tryServeFileCached(pathMod2.join(projectStaticPath, path2));
3058
+ const projectRes = await tryServeFileCached(pathMod2.join(projectStaticPath, path3));
2946
3059
  if (projectRes)
2947
3060
  return projectRes;
2948
- const routeClean = path2.replace(/(^\/|\/$)/g, "");
3061
+ const routeClean = path3.replace(/(^\/|\/$)/g, "");
2949
3062
  if (routeClean) {
2950
3063
  const routeRes = await tryServeFileCached(pathMod2.join(__dirname3, StaticPath, routeClean, "index.html"));
2951
3064
  if (routeRes)
@@ -2996,7 +3109,7 @@ var run = async (options) => {
2996
3109
  };
2997
3110
 
2998
3111
  // src/static.ts
2999
- import { cp, mkdir, writeFile } from "node:fs/promises";
3112
+ import { cp, mkdir, rm, writeFile } from "node:fs/promises";
3000
3113
  import { join, basename } from "node:path";
3001
3114
  async function renderStaticSite(opts) {
3002
3115
  const { ssrModule, htmlSource, staticSrc, paths, outputDir } = opts;
@@ -3038,12 +3151,28 @@ async function renderStaticSite(opts) {
3038
3151
  });
3039
3152
  }
3040
3153
  }
3154
+ if (opts.baseURL && rendered.length > 0) {
3155
+ const base = opts.baseURL.replace(/\/$/, "");
3156
+ const urls = rendered.map((p) => ` <url><loc>${base}${p}</loc></url>`).join(`
3157
+ `);
3158
+ const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
3159
+ ` + `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3160
+ ` + urls + `
3161
+ ` + "</urlset>";
3162
+ await writeFile(join(outputDir, "sitemap.xml"), sitemap, "utf-8");
3163
+ }
3041
3164
  const staticDest = join(outputDir, "static");
3042
3165
  await mkdir(staticDest, { recursive: true });
3043
3166
  await cp(staticSrc, staticDest, {
3044
3167
  recursive: true,
3045
3168
  filter: (src) => basename(src) !== "out.html"
3046
3169
  });
3170
+ const copiedImages = join(staticDest, "_images");
3171
+ const imagesOut = join(outputDir, "_images");
3172
+ try {
3173
+ await cp(copiedImages, imagesOut, { recursive: true });
3174
+ await rm(copiedImages, { recursive: true, force: true });
3175
+ } catch {}
3047
3176
  return { rendered, errors };
3048
3177
  }
3049
3178
 
@@ -3052,7 +3181,7 @@ var SUPPORTED = ["hadars.config.js", "hadars.config.mjs", "hadars.config.cjs", "
3052
3181
  function findConfig(cwd) {
3053
3182
  for (const name of SUPPORTED) {
3054
3183
  const p = resolve(cwd, name);
3055
- if (existsSync3(p))
3184
+ if (existsSync4(p))
3056
3185
  return p;
3057
3186
  }
3058
3187
  return null;
@@ -3128,11 +3257,11 @@ async function exportStatic(config, outputDir, cwd) {
3128
3257
  const ssrBundle = resolve(cwd, ".hadars", "index.ssr.js");
3129
3258
  const outHtml = resolve(cwd, ".hadars", "static", "out.html");
3130
3259
  const staticSrc = resolve(cwd, ".hadars", "static");
3131
- if (!existsSync3(ssrBundle)) {
3260
+ if (!existsSync4(ssrBundle)) {
3132
3261
  console.error(`SSR bundle not found: ${ssrBundle}`);
3133
3262
  process.exit(1);
3134
3263
  }
3135
- if (!existsSync3(outHtml)) {
3264
+ if (!existsSync4(outHtml)) {
3136
3265
  console.error(`HTML template not found: ${outHtml}`);
3137
3266
  process.exit(1);
3138
3267
  }
@@ -3168,12 +3297,13 @@ Source plugins require graphql-js to be installed:
3168
3297
  staticSrc,
3169
3298
  paths,
3170
3299
  outputDir: outDir,
3171
- graphql
3300
+ graphql,
3301
+ baseURL: config.baseURL
3172
3302
  });
3173
3303
  for (const p of rendered)
3174
3304
  console.log(` [200] ${p}`);
3175
- for (const { path: path2, error } of errors)
3176
- console.error(` [ERR] ${path2}: ${error.message}`);
3305
+ for (const { path: path3, error } of errors)
3306
+ console.error(` [ERR] ${path3}: ${error.message}`);
3177
3307
  console.log(`
3178
3308
  Exported to ${outputDir}/`);
3179
3309
  if (errors.length > 0)
@@ -3187,11 +3317,11 @@ async function bundleCloudflare(config, configPath, outputFile, cwd) {
3187
3317
  await build({ ...config, mode: "production" });
3188
3318
  const ssrBundle = resolve(cwd, ".hadars", "index.ssr.js");
3189
3319
  const outHtml = resolve(cwd, ".hadars", "static", "out.html");
3190
- if (!existsSync3(ssrBundle)) {
3320
+ if (!existsSync4(ssrBundle)) {
3191
3321
  console.error(`SSR bundle not found: ${ssrBundle}`);
3192
3322
  process.exit(1);
3193
3323
  }
3194
- if (!existsSync3(outHtml)) {
3324
+ if (!existsSync4(outHtml)) {
3195
3325
  console.error(`HTML template not found: ${outHtml}`);
3196
3326
  process.exit(1);
3197
3327
  }
@@ -3244,11 +3374,11 @@ async function bundleLambda(config, configPath, outputFile, cwd) {
3244
3374
  await build({ ...config, mode: "production" });
3245
3375
  const ssrBundle = resolve(cwd, ".hadars", "index.ssr.js");
3246
3376
  const outHtml = resolve(cwd, ".hadars", "static", "out.html");
3247
- if (!existsSync3(ssrBundle)) {
3377
+ if (!existsSync4(ssrBundle)) {
3248
3378
  console.error(`SSR bundle not found: ${ssrBundle}`);
3249
3379
  process.exit(1);
3250
3380
  }
3251
- if (!existsSync3(outHtml)) {
3381
+ if (!existsSync4(outHtml)) {
3252
3382
  console.error(`HTML template not found: ${outHtml}`);
3253
3383
  process.exit(1);
3254
3384
  }
@@ -3654,7 +3784,7 @@ dist/
3654
3784
  }
3655
3785
  async function createProject(name, cwd) {
3656
3786
  const dir = resolve(cwd, name);
3657
- if (existsSync3(dir)) {
3787
+ if (existsSync4(dir)) {
3658
3788
  console.error(`Directory already exists: ${dir}`);
3659
3789
  process.exit(1);
3660
3790
  }
@@ -1188,8 +1188,8 @@ var ATTR = {
1188
1188
  fetchPriority: "fetchpriority",
1189
1189
  hrefLang: "hreflang"
1190
1190
  };
1191
- function renderHeadTag(tag, id, opts, selfClose = false) {
1192
- let attrs = ` id="${escAttr(id)}"`;
1191
+ function renderHeadTag(tag, _id, opts, selfClose = false) {
1192
+ let attrs = "";
1193
1193
  let inner = "";
1194
1194
  for (const [k, v] of Object.entries(opts)) {
1195
1195
  if (k === "key" || k === "children") continue;
@@ -1369,6 +1369,7 @@ async function buildCacheEntry(res, ttl) {
1369
1369
  }
1370
1370
  });
1371
1371
  headers.push(["content-encoding", "gzip"]);
1372
+ headers.push(["vary", "accept-encoding"]);
1372
1373
  return { body, status: res.status, headers, expiresAt: ttl != null ? Date.now() + ttl : null };
1373
1374
  }
1374
1375
  async function serveFromEntry(entry, req) {
@@ -1430,15 +1431,18 @@ function createCloudflareHandler(options, bundled) {
1430
1431
  if (proxied) return proxied;
1431
1432
  try {
1432
1433
  const { default: Component, getInitProps, getFinalProps } = ssrModule;
1434
+ const isDataOnly = request.headers.get("Accept") === "application/json";
1433
1435
  const { head, status, getAppBody, finalize } = await getReactResponse(request, {
1434
1436
  document: {
1435
1437
  body: Component,
1436
1438
  lang: "en",
1437
1439
  getInitProps,
1438
1440
  getFinalProps
1439
- }
1441
+ },
1442
+ singlePass: !isDataOnly,
1443
+ dataOnly: isDataOnly
1440
1444
  });
1441
- if (request.headers.get("Accept") === "application/json") {
1445
+ if (isDataOnly) {
1442
1446
  const { clientProps: clientProps2 } = await finalize();
1443
1447
  const serverData = clientProps2.__serverData ?? {};
1444
1448
  return new Response(JSON.stringify({ serverData }), {
@@ -1,4 +1,4 @@
1
- import { H as HadarsEntryModule, a as HadarsOptions } from './hadars-CPplIz_z.cjs';
1
+ import { H as HadarsEntryModule, a as HadarsOptions } from './hadars-gkIw8CFo.cjs';
2
2
 
3
3
  /**
4
4
  * Cloudflare Workers adapter for hadars.
@@ -1,4 +1,4 @@
1
- import { H as HadarsEntryModule, a as HadarsOptions } from './hadars-CPplIz_z.js';
1
+ import { H as HadarsEntryModule, a as HadarsOptions } from './hadars-gkIw8CFo.js';
2
2
 
3
3
  /**
4
4
  * Cloudflare Workers adapter for hadars.
@@ -6,7 +6,7 @@ import {
6
6
  getReactResponse,
7
7
  makePrecontentHtmlGetter,
8
8
  parseRequest
9
- } from "./chunk-VAR5KTG3.js";
9
+ } from "./chunk-QGDQJUIU.js";
10
10
  import "./chunk-6SOA2HTO.js";
11
11
  import "./chunk-OZUZS2PD.js";
12
12
 
@@ -27,15 +27,18 @@ function createCloudflareHandler(options, bundled) {
27
27
  if (proxied) return proxied;
28
28
  try {
29
29
  const { default: Component, getInitProps, getFinalProps } = ssrModule;
30
+ const isDataOnly = request.headers.get("Accept") === "application/json";
30
31
  const { head, status, getAppBody, finalize } = await getReactResponse(request, {
31
32
  document: {
32
33
  body: Component,
33
34
  lang: "en",
34
35
  getInitProps,
35
36
  getFinalProps
36
- }
37
+ },
38
+ singlePass: !isDataOnly,
39
+ dataOnly: isDataOnly
37
40
  });
38
- if (request.headers.get("Accept") === "application/json") {
41
+ if (isDataOnly) {
39
42
  const { clientProps: clientProps2 } = await finalize();
40
43
  const serverData = clientProps2.__serverData ?? {};
41
44
  return new Response(JSON.stringify({ serverData }), {
@@ -274,6 +274,30 @@ interface HadarsOptions {
274
274
  * onError: (err, req) => console.error('[myapp]', req.method, req.url, err)
275
275
  */
276
276
  onError?: (err: Error, req: Request) => void | Promise<void>;
277
+ /**
278
+ * Build-time image variant generation. When set, `hadars build` and
279
+ * `hadars export static` scan the project's `static/` directory and produce
280
+ * resized variants for every raster image (JPG, PNG, WebP, AVIF, TIFF, BMP).
281
+ *
282
+ * Variants are written to `/_images/<original-path>-<width>.<format>` and
283
+ * served automatically by the production server. Use the `<HadarsImage>`
284
+ * component (exported from `hadars`) to generate `<picture>` elements that
285
+ * reference these variants with proper `srcset` / `sizes` attributes.
286
+ *
287
+ * Requires `sharp` to be installed as a project dependency:
288
+ * npm install sharp
289
+ *
290
+ * @example
291
+ * images: { widths: [640, 1280, 1920], formats: ['webp', 'avif'], quality: 85 }
292
+ */
293
+ images?: {
294
+ /** Pixel widths to generate per source image. Default: `[640, 1280, 1920]` */
295
+ widths?: number[];
296
+ /** Output formats. Default: `['webp']` */
297
+ formats?: ('webp' | 'avif')[];
298
+ /** Encoding quality 1–100. Default: `80` */
299
+ quality?: number;
300
+ };
277
301
  }
278
302
  /**
279
303
  * A Gatsby-compatible source plugin entry, matching the format used in
@@ -274,6 +274,30 @@ interface HadarsOptions {
274
274
  * onError: (err, req) => console.error('[myapp]', req.method, req.url, err)
275
275
  */
276
276
  onError?: (err: Error, req: Request) => void | Promise<void>;
277
+ /**
278
+ * Build-time image variant generation. When set, `hadars build` and
279
+ * `hadars export static` scan the project's `static/` directory and produce
280
+ * resized variants for every raster image (JPG, PNG, WebP, AVIF, TIFF, BMP).
281
+ *
282
+ * Variants are written to `/_images/<original-path>-<width>.<format>` and
283
+ * served automatically by the production server. Use the `<HadarsImage>`
284
+ * component (exported from `hadars`) to generate `<picture>` elements that
285
+ * reference these variants with proper `srcset` / `sizes` attributes.
286
+ *
287
+ * Requires `sharp` to be installed as a project dependency:
288
+ * npm install sharp
289
+ *
290
+ * @example
291
+ * images: { widths: [640, 1280, 1920], formats: ['webp', 'avif'], quality: 85 }
292
+ */
293
+ images?: {
294
+ /** Pixel widths to generate per source image. Default: `[640, 1280, 1920]` */
295
+ widths?: number[];
296
+ /** Output formats. Default: `['webp']` */
297
+ formats?: ('webp' | 'avif')[];
298
+ /** Encoding quality 1–100. Default: `80` */
299
+ quality?: number;
300
+ };
277
301
  }
278
302
  /**
279
303
  * A Gatsby-compatible source plugin entry, matching the format used in
package/dist/index.cjs CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  HadarsHead: () => Head,
34
+ HadarsImage: () => HadarsImage,
34
35
  initServerDataCache: () => initServerDataCache,
35
36
  loadModule: () => loadModule,
36
37
  useGraphQL: () => useGraphQL,
@@ -344,6 +345,55 @@ var Head = import_react.default.memo(({ children, status }) => {
344
345
  return null;
345
346
  });
346
347
 
348
+ // src/components/Image.tsx
349
+ var import_react2 = require("react");
350
+ var import_jsx_runtime = require("react/jsx-runtime");
351
+ var FORMAT_MIME = {
352
+ webp: "image/webp",
353
+ avif: "image/avif"
354
+ };
355
+ function HadarsImage({
356
+ src,
357
+ alt,
358
+ widths = [640, 1280, 1920],
359
+ formats = ["webp"],
360
+ sizes = "100vw",
361
+ loading = "lazy",
362
+ decoding = "async",
363
+ ...rest
364
+ }) {
365
+ const dotIdx = src.lastIndexOf(".");
366
+ const srcNoExt = dotIdx !== -1 ? src.slice(0, dotIdx) : src;
367
+ const imgBase = "/_images" + srcNoExt;
368
+ const buildSrcSet = (fmt) => widths.map((w) => `${imgBase}-${w}.${fmt} ${w}w`).join(", ");
369
+ const orderedFormats = [...formats].sort((a, b) => {
370
+ const rank = { avif: 0, webp: 1 };
371
+ return (rank[a] ?? 2) - (rank[b] ?? 2);
372
+ });
373
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("picture", { children: [
374
+ orderedFormats.map((fmt) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
375
+ "source",
376
+ {
377
+ type: FORMAT_MIME[fmt] ?? `image/${fmt}`,
378
+ srcSet: buildSrcSet(fmt),
379
+ sizes
380
+ },
381
+ fmt
382
+ )),
383
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
384
+ "img",
385
+ {
386
+ src,
387
+ alt,
388
+ loading,
389
+ decoding,
390
+ sizes,
391
+ ...rest
392
+ }
393
+ )
394
+ ] });
395
+ }
396
+
347
397
  // src/index.tsx
348
398
  function loadModule(path) {
349
399
  return import(
@@ -354,6 +404,7 @@ function loadModule(path) {
354
404
  // Annotate the CommonJS export names for ESM import in node:
355
405
  0 && (module.exports = {
356
406
  HadarsHead,
407
+ HadarsImage,
357
408
  initServerDataCache,
358
409
  loadModule,
359
410
  useGraphQL,
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { b as HadarsDocumentNode } from './hadars-CPplIz_z.cjs';
2
- export { G as GraphQLExecutor, c as HadarsApp, H as HadarsEntryModule, d as HadarsGetClientProps, e as HadarsGetFinalProps, f as HadarsGetInitialProps, a as HadarsOptions, g as HadarsProps, h as HadarsRequest, i as HadarsSourceEntry, j as HadarsStaticContext } from './hadars-CPplIz_z.cjs';
1
+ import { b as HadarsDocumentNode } from './hadars-gkIw8CFo.cjs';
2
+ export { G as GraphQLExecutor, c as HadarsApp, H as HadarsEntryModule, d as HadarsGetClientProps, e as HadarsGetFinalProps, f as HadarsGetInitialProps, a as HadarsOptions, g as HadarsProps, h as HadarsRequest, i as HadarsSourceEntry, j as HadarsStaticContext } from './hadars-gkIw8CFo.cjs';
3
3
  import React from 'react';
4
4
 
5
5
  /** Call this before hydrating to seed the client cache from the server's data.
@@ -68,6 +68,55 @@ declare const Head: React.FC<{
68
68
  status?: number;
69
69
  }>;
70
70
 
71
+ interface HadarsImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
72
+ src: string;
73
+ alt: string;
74
+ /**
75
+ * Pixel widths to include in the `srcset`. Must match (or be a subset of)
76
+ * the `images.widths` configured in `hadars.config.ts`.
77
+ * Default: `[640, 1280, 1920]`.
78
+ */
79
+ widths?: number[];
80
+ /**
81
+ * Formats to offer, in preference order (most-preferred first).
82
+ * Browsers pick the first format they support.
83
+ * Default: `['webp']`.
84
+ */
85
+ formats?: ('webp' | 'avif')[];
86
+ /**
87
+ * `sizes` attribute forwarded to both `<source>` and the fallback `<img>`.
88
+ * Default: `'100vw'`.
89
+ */
90
+ sizes?: string;
91
+ }
92
+ /**
93
+ * Responsive image component.
94
+ *
95
+ * Renders a `<picture>` element with `<source>` elements pointing at the
96
+ * build-time variants generated by `hadars build` or `hadars export static`.
97
+ * Variants are served from `/_images/<src-without-ext>-<width>.<format>`.
98
+ *
99
+ * Configure variant generation in `hadars.config.ts`:
100
+ * ```ts
101
+ * import type { HadarsOptions } from 'hadars';
102
+ * export default {
103
+ * images: { widths: [640, 1280, 1920], formats: ['webp', 'avif'], quality: 85 },
104
+ * } satisfies HadarsOptions;
105
+ * ```
106
+ *
107
+ * Falls back to the original `src` for browsers without `<picture>` support,
108
+ * and always sets `loading="lazy"` and `decoding="async"` unless overridden.
109
+ *
110
+ * @example
111
+ * <HadarsImage
112
+ * src="/photos/hero.jpg"
113
+ * alt="A scenic mountain view"
114
+ * widths={[640, 1280, 1920]}
115
+ * sizes="(max-width: 768px) 100vw, 50vw"
116
+ * />
117
+ */
118
+ declare function HadarsImage({ src, alt, widths, formats, sizes, loading, decoding, ...rest }: HadarsImageProps): React.ReactElement;
119
+
71
120
  /**
72
121
  * Dynamically loads a module with target-aware behaviour:
73
122
  *
@@ -92,4 +141,4 @@ declare const Head: React.FC<{
92
141
  */
93
142
  declare function loadModule<T = any>(path: string): Promise<T>;
94
143
 
95
- export { HadarsDocumentNode, Head as HadarsHead, initServerDataCache, loadModule, useGraphQL, useServerData };
144
+ export { HadarsDocumentNode, Head as HadarsHead, HadarsImage, type HadarsImageProps, initServerDataCache, loadModule, useGraphQL, useServerData };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { b as HadarsDocumentNode } from './hadars-CPplIz_z.js';
2
- export { G as GraphQLExecutor, c as HadarsApp, H as HadarsEntryModule, d as HadarsGetClientProps, e as HadarsGetFinalProps, f as HadarsGetInitialProps, a as HadarsOptions, g as HadarsProps, h as HadarsRequest, i as HadarsSourceEntry, j as HadarsStaticContext } from './hadars-CPplIz_z.js';
1
+ import { b as HadarsDocumentNode } from './hadars-gkIw8CFo.js';
2
+ export { G as GraphQLExecutor, c as HadarsApp, H as HadarsEntryModule, d as HadarsGetClientProps, e as HadarsGetFinalProps, f as HadarsGetInitialProps, a as HadarsOptions, g as HadarsProps, h as HadarsRequest, i as HadarsSourceEntry, j as HadarsStaticContext } from './hadars-gkIw8CFo.js';
3
3
  import React from 'react';
4
4
 
5
5
  /** Call this before hydrating to seed the client cache from the server's data.
@@ -68,6 +68,55 @@ declare const Head: React.FC<{
68
68
  status?: number;
69
69
  }>;
70
70
 
71
+ interface HadarsImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
72
+ src: string;
73
+ alt: string;
74
+ /**
75
+ * Pixel widths to include in the `srcset`. Must match (or be a subset of)
76
+ * the `images.widths` configured in `hadars.config.ts`.
77
+ * Default: `[640, 1280, 1920]`.
78
+ */
79
+ widths?: number[];
80
+ /**
81
+ * Formats to offer, in preference order (most-preferred first).
82
+ * Browsers pick the first format they support.
83
+ * Default: `['webp']`.
84
+ */
85
+ formats?: ('webp' | 'avif')[];
86
+ /**
87
+ * `sizes` attribute forwarded to both `<source>` and the fallback `<img>`.
88
+ * Default: `'100vw'`.
89
+ */
90
+ sizes?: string;
91
+ }
92
+ /**
93
+ * Responsive image component.
94
+ *
95
+ * Renders a `<picture>` element with `<source>` elements pointing at the
96
+ * build-time variants generated by `hadars build` or `hadars export static`.
97
+ * Variants are served from `/_images/<src-without-ext>-<width>.<format>`.
98
+ *
99
+ * Configure variant generation in `hadars.config.ts`:
100
+ * ```ts
101
+ * import type { HadarsOptions } from 'hadars';
102
+ * export default {
103
+ * images: { widths: [640, 1280, 1920], formats: ['webp', 'avif'], quality: 85 },
104
+ * } satisfies HadarsOptions;
105
+ * ```
106
+ *
107
+ * Falls back to the original `src` for browsers without `<picture>` support,
108
+ * and always sets `loading="lazy"` and `decoding="async"` unless overridden.
109
+ *
110
+ * @example
111
+ * <HadarsImage
112
+ * src="/photos/hero.jpg"
113
+ * alt="A scenic mountain view"
114
+ * widths={[640, 1280, 1920]}
115
+ * sizes="(max-width: 768px) 100vw, 50vw"
116
+ * />
117
+ */
118
+ declare function HadarsImage({ src, alt, widths, formats, sizes, loading, decoding, ...rest }: HadarsImageProps): React.ReactElement;
119
+
71
120
  /**
72
121
  * Dynamically loads a module with target-aware behaviour:
73
122
  *
@@ -92,4 +141,4 @@ declare const Head: React.FC<{
92
141
  */
93
142
  declare function loadModule<T = any>(path: string): Promise<T>;
94
143
 
95
- export { HadarsDocumentNode, Head as HadarsHead, initServerDataCache, loadModule, useGraphQL, useServerData };
144
+ export { HadarsDocumentNode, Head as HadarsHead, HadarsImage, type HadarsImageProps, initServerDataCache, loadModule, useGraphQL, useServerData };
package/dist/index.js CHANGED
@@ -304,6 +304,55 @@ var Head = React.memo(({ children, status }) => {
304
304
  return null;
305
305
  });
306
306
 
307
+ // src/components/Image.tsx
308
+ import "react";
309
+ import { jsx, jsxs } from "react/jsx-runtime";
310
+ var FORMAT_MIME = {
311
+ webp: "image/webp",
312
+ avif: "image/avif"
313
+ };
314
+ function HadarsImage({
315
+ src,
316
+ alt,
317
+ widths = [640, 1280, 1920],
318
+ formats = ["webp"],
319
+ sizes = "100vw",
320
+ loading = "lazy",
321
+ decoding = "async",
322
+ ...rest
323
+ }) {
324
+ const dotIdx = src.lastIndexOf(".");
325
+ const srcNoExt = dotIdx !== -1 ? src.slice(0, dotIdx) : src;
326
+ const imgBase = "/_images" + srcNoExt;
327
+ const buildSrcSet = (fmt) => widths.map((w) => `${imgBase}-${w}.${fmt} ${w}w`).join(", ");
328
+ const orderedFormats = [...formats].sort((a, b) => {
329
+ const rank = { avif: 0, webp: 1 };
330
+ return (rank[a] ?? 2) - (rank[b] ?? 2);
331
+ });
332
+ return /* @__PURE__ */ jsxs("picture", { children: [
333
+ orderedFormats.map((fmt) => /* @__PURE__ */ jsx(
334
+ "source",
335
+ {
336
+ type: FORMAT_MIME[fmt] ?? `image/${fmt}`,
337
+ srcSet: buildSrcSet(fmt),
338
+ sizes
339
+ },
340
+ fmt
341
+ )),
342
+ /* @__PURE__ */ jsx(
343
+ "img",
344
+ {
345
+ src,
346
+ alt,
347
+ loading,
348
+ decoding,
349
+ sizes,
350
+ ...rest
351
+ }
352
+ )
353
+ ] });
354
+ }
355
+
307
356
  // src/index.tsx
308
357
  function loadModule(path) {
309
358
  return import(
@@ -313,6 +362,7 @@ function loadModule(path) {
313
362
  }
314
363
  export {
315
364
  Head as HadarsHead,
365
+ HadarsImage,
316
366
  initServerDataCache,
317
367
  loadModule,
318
368
  useGraphQL,
package/dist/lambda.cjs CHANGED
@@ -1228,8 +1228,8 @@ var ATTR = {
1228
1228
  fetchPriority: "fetchpriority",
1229
1229
  hrefLang: "hreflang"
1230
1230
  };
1231
- function renderHeadTag(tag, id, opts, selfClose = false) {
1232
- let attrs = ` id="${escAttr(id)}"`;
1231
+ function renderHeadTag(tag, _id, opts, selfClose = false) {
1232
+ let attrs = "";
1233
1233
  let inner = "";
1234
1234
  for (const [k, v] of Object.entries(opts)) {
1235
1235
  if (k === "key" || k === "children") continue;
@@ -1409,6 +1409,7 @@ async function buildCacheEntry(res, ttl) {
1409
1409
  }
1410
1410
  });
1411
1411
  headers.push(["content-encoding", "gzip"]);
1412
+ headers.push(["vary", "accept-encoding"]);
1412
1413
  return { body, status: res.status, headers, expiresAt: ttl != null ? Date.now() + ttl : null };
1413
1414
  }
1414
1415
  async function serveFromEntry(entry, req) {
@@ -1553,15 +1554,18 @@ function createLambdaHandler(options, bundled) {
1553
1554
  getInitProps,
1554
1555
  getFinalProps
1555
1556
  } = await getSsrModule();
1557
+ const isDataOnly = request.headers.get("Accept") === "application/json";
1556
1558
  const { head, status, getAppBody, finalize } = await getReactResponse(request, {
1557
1559
  document: {
1558
1560
  body: Component,
1559
1561
  lang: "en",
1560
1562
  getInitProps,
1561
1563
  getFinalProps
1562
- }
1564
+ },
1565
+ singlePass: !isDataOnly,
1566
+ dataOnly: isDataOnly
1563
1567
  });
1564
- if (request.headers.get("Accept") === "application/json") {
1568
+ if (isDataOnly) {
1565
1569
  const { clientProps: clientProps2 } = await finalize();
1566
1570
  const serverData = clientProps2.__serverData ?? {};
1567
1571
  return new Response(JSON.stringify({ serverData }), {
package/dist/lambda.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { H as HadarsEntryModule, a as HadarsOptions } from './hadars-CPplIz_z.cjs';
1
+ import { H as HadarsEntryModule, a as HadarsOptions } from './hadars-gkIw8CFo.cjs';
2
2
 
3
3
  /**
4
4
  * AWS Lambda adapter for hadars.
package/dist/lambda.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { H as HadarsEntryModule, a as HadarsOptions } from './hadars-CPplIz_z.js';
1
+ import { H as HadarsEntryModule, a as HadarsOptions } from './hadars-gkIw8CFo.js';
2
2
 
3
3
  /**
4
4
  * AWS Lambda adapter for hadars.
package/dist/lambda.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  getReactResponse,
7
7
  makePrecontentHtmlGetter,
8
8
  parseRequest
9
- } from "./chunk-VAR5KTG3.js";
9
+ } from "./chunk-QGDQJUIU.js";
10
10
  import "./chunk-6SOA2HTO.js";
11
11
  import "./chunk-OZUZS2PD.js";
12
12
 
@@ -152,15 +152,18 @@ function createLambdaHandler(options, bundled) {
152
152
  getInitProps,
153
153
  getFinalProps
154
154
  } = await getSsrModule();
155
+ const isDataOnly = request.headers.get("Accept") === "application/json";
155
156
  const { head, status, getAppBody, finalize } = await getReactResponse(request, {
156
157
  document: {
157
158
  body: Component,
158
159
  lang: "en",
159
160
  getInitProps,
160
161
  getFinalProps
161
- }
162
+ },
163
+ singlePass: !isDataOnly,
164
+ dataOnly: isDataOnly
162
165
  });
163
- if (request.headers.get("Accept") === "application/json") {
166
+ if (isDataOnly) {
164
167
  const { clientProps: clientProps2 } = await finalize();
165
168
  const serverData = clientProps2.__serverData ?? {};
166
169
  return new Response(JSON.stringify({ serverData }), {
@@ -1026,8 +1026,8 @@ var ATTR = {
1026
1026
  fetchPriority: "fetchpriority",
1027
1027
  hrefLang: "hreflang"
1028
1028
  };
1029
- function renderHeadTag(tag, id, opts, selfClose = false) {
1030
- let attrs = ` id="${escAttr(id)}"`;
1029
+ function renderHeadTag(tag, _id, opts, selfClose = false) {
1030
+ let attrs = "";
1031
1031
  let inner = "";
1032
1032
  for (const [k, v] of Object.entries(opts)) {
1033
1033
  if (k === "key" || k === "children")
@@ -1114,15 +1114,35 @@ async function runFullLifecycle(serialReq) {
1114
1114
  const html = `<div id="app">${appHtml}</div><script id="hadars" type="application/json">${scriptContent}</script>`;
1115
1115
  return { html, headHtml, status };
1116
1116
  }
1117
- parentPort.on("message", async (msg) => {
1118
- const { id, type, request } = msg;
1117
+ var _draining = false;
1118
+ var _queue = [];
1119
+ function enqueue(task) {
1120
+ _queue.push(task);
1121
+ if (!_draining)
1122
+ drain();
1123
+ }
1124
+ async function drain() {
1125
+ _draining = true;
1119
1126
  try {
1120
- await init();
1121
- if (type !== "renderFull")
1122
- return;
1123
- const { html, headHtml, status } = await runFullLifecycle(request);
1124
- parentPort.postMessage({ id, html, headHtml, status });
1125
- } catch (err) {
1126
- parentPort.postMessage({ id, error: err?.message ?? String(err) });
1127
+ while (_queue.length > 0) {
1128
+ const task = _queue.shift();
1129
+ await task();
1130
+ }
1131
+ } finally {
1132
+ _draining = false;
1127
1133
  }
1134
+ }
1135
+ parentPort.on("message", (msg) => {
1136
+ const { id, type, request } = msg;
1137
+ enqueue(async () => {
1138
+ try {
1139
+ await init();
1140
+ if (type !== "renderFull")
1141
+ return;
1142
+ const { html, headHtml, status } = await runFullLifecycle(request);
1143
+ parentPort.postMessage({ id, html, headHtml, status });
1144
+ } catch (err) {
1145
+ parentPort.postMessage({ id, error: err?.message ?? String(err), stack: err?.stack });
1146
+ }
1147
+ });
1128
1148
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hadars",
3
- "version": "1.0.7",
3
+ "version": "1.0.10",
4
4
  "description": "Minimal SSR framework for React — rspack, HMR, TypeScript, Bun/Node/Deno",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",