hadars 0.1.35 → 0.1.37

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.
@@ -0,0 +1 @@
1
+ export { b as Fragment, j as jsx, j as jsxDEV, j as jsxs } from '../jsx-runtime-BOYrELJb.cjs';
@@ -1 +1 @@
1
- export { F as Fragment, j as jsx, j as jsxDEV, j as jsxs } from '../jsx-runtime-97ca74a5.js';
1
+ export { b as Fragment, j as jsx, j as jsxDEV, j as jsxs } from '../jsx-runtime-BOYrELJb.js';
@@ -26,14 +26,12 @@ function processSegmentCache(html) {
26
26
  const cacheM = /data-cache="([^"]+)"/.exec(attrs);
27
27
  const keyM = /data-key="([^"]+)"/.exec(attrs);
28
28
  const ttlM = /data-ttl="(\d+)"/.exec(attrs);
29
- if (!cacheM || !keyM)
30
- return match;
29
+ if (!cacheM || !keyM) return match;
31
30
  if (cacheM[1] === "miss") {
32
31
  setSegment(keyM[1], content, ttlM ? Number(ttlM[1]) : void 0);
33
32
  return content;
34
33
  }
35
- if (cacheM[1] === "hit")
36
- return content;
34
+ if (cacheM[1] === "hit") return content;
37
35
  return match;
38
36
  }
39
37
  );
@@ -42,10 +40,10 @@ function processSegmentCache(html) {
42
40
  }
43
41
 
44
42
  // src/slim-react/types.ts
45
- var SLIM_ELEMENT = Symbol.for("react.element");
46
- var REACT19_ELEMENT = Symbol.for("react.transitional.element");
47
- var FRAGMENT_TYPE = Symbol.for("react.fragment");
48
- var SUSPENSE_TYPE = Symbol.for("react.suspense");
43
+ var SLIM_ELEMENT = /* @__PURE__ */ Symbol.for("react.element");
44
+ var REACT19_ELEMENT = /* @__PURE__ */ Symbol.for("react.transitional.element");
45
+ var FRAGMENT_TYPE = /* @__PURE__ */ Symbol.for("react.fragment");
46
+ var SUSPENSE_TYPE = /* @__PURE__ */ Symbol.for("react.suspense");
49
47
 
50
48
  // src/slim-react/jsx.ts
51
49
  function createElement(type, props, ...children) {
@@ -68,8 +66,7 @@ function createElement(type, props, ...children) {
68
66
  // src/slim-react/renderContext.ts
69
67
  var MAP_KEY = "__slimReactContextMap";
70
68
  var _g = globalThis;
71
- if (!("__slimReactContextMap" in _g))
72
- _g[MAP_KEY] = null;
69
+ if (!("__slimReactContextMap" in _g)) _g[MAP_KEY] = null;
73
70
  function swapContextMap(map) {
74
71
  const prev = _g[MAP_KEY];
75
72
  _g[MAP_KEY] = map;
@@ -80,8 +77,7 @@ function captureMap() {
80
77
  }
81
78
  function getContextValue(context) {
82
79
  const map = _g[MAP_KEY];
83
- if (map && map.has(context))
84
- return map.get(context);
80
+ if (map && map.has(context)) return map.get(context);
85
81
  const c = context;
86
82
  return "_defaultValue" in c ? c._defaultValue : c._currentValue;
87
83
  }
@@ -163,8 +159,7 @@ function restoreContext(snap) {
163
159
  }
164
160
  function getTreeId() {
165
161
  const { id, overflow } = s().currentTreeContext;
166
- if (id === 1)
167
- return overflow;
162
+ if (id === 1) return overflow;
168
163
  const stripped = (id & ~(1 << 31 - Math.clz32(id))).toString(32);
169
164
  return stripped + overflow;
170
165
  }
@@ -173,8 +168,7 @@ function makeId() {
173
168
  const treeId = getTreeId();
174
169
  const n = st.localIdCounter++;
175
170
  let id = "_R_" + st.idPrefix + treeId;
176
- if (n > 0)
177
- id += "H" + n.toString(32);
171
+ if (n > 0) id += "H" + n.toString(32);
178
172
  return id + "_";
179
173
  }
180
174
 
@@ -229,10 +223,8 @@ function use(usable) {
229
223
  return getContextValue(usable);
230
224
  }
231
225
  const promise = usable;
232
- if (promise.status === "fulfilled")
233
- return promise.value;
234
- if (promise.status === "rejected")
235
- throw promise.reason;
226
+ if (promise.status === "fulfilled") return promise.value;
227
+ if (promise.status === "rejected") throw promise.reason;
236
228
  throw promise;
237
229
  }
238
230
 
@@ -266,15 +258,13 @@ var slimDispatcher = {
266
258
  useHostTransitionStatus: () => false
267
259
  };
268
260
  function installDispatcher() {
269
- if (!_internals)
270
- return null;
261
+ if (!_internals) return null;
271
262
  const prev = _internals.H;
272
263
  _internals.H = slimDispatcher;
273
264
  return prev;
274
265
  }
275
266
  function restoreDispatcher(prev) {
276
- if (_internals)
277
- _internals.H = prev;
267
+ if (_internals) _internals.H = prev;
278
268
  }
279
269
 
280
270
  // src/slim-react/render.ts
@@ -305,8 +295,7 @@ function escapeAttr(str) {
305
295
  function styleObjectToString(style) {
306
296
  let result = "";
307
297
  for (const key in style) {
308
- if (result)
309
- result += ";";
298
+ if (result) result += ";";
310
299
  const cssKey = key.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
311
300
  result += cssKey + ":" + style[key];
312
301
  }
@@ -473,8 +462,7 @@ function renderAttributes(props, isSvg) {
473
462
  }
474
463
  if (key === "style" && typeof value === "object") {
475
464
  const styleStr = styleObjectToString(value);
476
- if (styleStr)
477
- attrs += ` style="${escapeAttr(styleStr)}"`;
465
+ if (styleStr) attrs += ` style="${escapeAttr(styleStr)}"`;
478
466
  continue;
479
467
  }
480
468
  attrs += ` ${attrName}="${escapeAttr(String(value))}"`;
@@ -493,14 +481,12 @@ var BufferWriter = class {
493
481
  this.lastWasText = true;
494
482
  }
495
483
  flush(target) {
496
- for (const c of this.chunks)
497
- target.write(c);
484
+ for (const c of this.chunks) target.write(c);
498
485
  target.lastWasText = this.lastWasText;
499
486
  }
500
487
  };
501
488
  function renderNode(node, writer, isSvg = false) {
502
- if (node == null || typeof node === "boolean")
503
- return;
489
+ if (node == null || typeof node === "boolean") return;
504
490
  if (typeof node === "string") {
505
491
  writer.text(escapeHtml(node));
506
492
  return;
@@ -521,8 +507,7 @@ function renderNode(node, writer, isSvg = false) {
521
507
  }
522
508
  if (typeof node === "object" && node !== null && "$$typeof" in node) {
523
509
  const elType = node["$$typeof"];
524
- if (elType !== SLIM_ELEMENT && elType !== REACT19_ELEMENT)
525
- return;
510
+ if (elType !== SLIM_ELEMENT && elType !== REACT19_ELEMENT) return;
526
511
  const element = node;
527
512
  const { type, props } = element;
528
513
  if (type === FRAGMENT_TYPE) {
@@ -543,17 +528,14 @@ function renderNode(node, writer, isSvg = false) {
543
528
  }
544
529
  }
545
530
  function markSelectedOptionsMulti(children, selectedValues) {
546
- if (children == null || typeof children === "boolean")
547
- return children;
548
- if (typeof children === "string" || typeof children === "number")
549
- return children;
531
+ if (children == null || typeof children === "boolean") return children;
532
+ if (typeof children === "string" || typeof children === "number") return children;
550
533
  if (Array.isArray(children)) {
551
534
  return children.map((c) => markSelectedOptionsMulti(c, selectedValues));
552
535
  }
553
536
  if (typeof children === "object" && "$$typeof" in children) {
554
537
  const elType = children["$$typeof"];
555
- if (elType !== SLIM_ELEMENT && elType !== REACT19_ELEMENT)
556
- return children;
538
+ if (elType !== SLIM_ELEMENT && elType !== REACT19_ELEMENT) return children;
557
539
  const el = children;
558
540
  if (el.type === "option") {
559
541
  const optValue = el.props.value !== void 0 ? el.props.value : el.props.children;
@@ -574,8 +556,7 @@ function renderHostElement(tag, props, writer, isSvg) {
574
556
  const textContent = props.value ?? props.defaultValue ?? props.children ?? "";
575
557
  const filteredProps = {};
576
558
  for (const k of Object.keys(props)) {
577
- if (k !== "value" && k !== "defaultValue" && k !== "children")
578
- filteredProps[k] = props[k];
559
+ if (k !== "value" && k !== "defaultValue" && k !== "children") filteredProps[k] = props[k];
579
560
  }
580
561
  writer.write(`<textarea${renderAttributes(filteredProps, false)}>`);
581
562
  writer.text(escapeHtml(String(textContent)));
@@ -586,8 +567,7 @@ function renderHostElement(tag, props, writer, isSvg) {
586
567
  const selectedValue = props.value ?? props.defaultValue;
587
568
  const filteredProps = {};
588
569
  for (const k of Object.keys(props)) {
589
- if (k !== "value" && k !== "defaultValue")
590
- filteredProps[k] = props[k];
570
+ if (k !== "value" && k !== "defaultValue") filteredProps[k] = props[k];
591
571
  }
592
572
  writer.write(`<select${renderAttributes(filteredProps, false)}>`);
593
573
  const selectedSet = selectedValue == null ? null : Array.isArray(selectedValue) ? new Set(selectedValue.map(String)) : /* @__PURE__ */ new Set([String(selectedValue)]);
@@ -621,12 +601,12 @@ function renderHostElement(tag, props, writer, isSvg) {
621
601
  }
622
602
  writer.write(`</${tag}>`);
623
603
  }
624
- var REACT_MEMO = Symbol.for("react.memo");
625
- var REACT_FORWARD_REF = Symbol.for("react.forward_ref");
626
- var REACT_PROVIDER = Symbol.for("react.provider");
627
- var REACT_CONTEXT = Symbol.for("react.context");
628
- var REACT_CONSUMER = Symbol.for("react.consumer");
629
- var REACT_LAZY = Symbol.for("react.lazy");
604
+ var REACT_MEMO = /* @__PURE__ */ Symbol.for("react.memo");
605
+ var REACT_FORWARD_REF = /* @__PURE__ */ Symbol.for("react.forward_ref");
606
+ var REACT_PROVIDER = /* @__PURE__ */ Symbol.for("react.provider");
607
+ var REACT_CONTEXT = /* @__PURE__ */ Symbol.for("react.context");
608
+ var REACT_CONSUMER = /* @__PURE__ */ Symbol.for("react.consumer");
609
+ var REACT_LAZY = /* @__PURE__ */ Symbol.for("react.lazy");
630
610
  function renderComponent(type, props, writer, isSvg) {
631
611
  const typeOf = type?.$$typeof;
632
612
  if (typeOf === REACT_MEMO) {
@@ -695,8 +675,7 @@ function renderComponent(type, props, writer, isSvg) {
695
675
  const instance = new type(props);
696
676
  if (typeof type.getDerivedStateFromProps === "function") {
697
677
  const derived = type.getDerivedStateFromProps(props, instance.state ?? {});
698
- if (derived != null)
699
- instance.state = { ...instance.state ?? {}, ...derived };
678
+ if (derived != null) instance.state = { ...instance.state ?? {}, ...derived };
700
679
  }
701
680
  result = instance.render();
702
681
  } else {
@@ -705,8 +684,7 @@ function renderComponent(type, props, writer, isSvg) {
705
684
  } catch (e) {
706
685
  restoreDispatcher(prevDispatcher);
707
686
  popComponentScope(savedScope);
708
- if (isProvider)
709
- popContextValue(ctx, prevCtxValue);
687
+ if (isProvider) popContextValue(ctx, prevCtxValue);
710
688
  throw e;
711
689
  }
712
690
  restoreDispatcher(prevDispatcher);
@@ -715,11 +693,9 @@ function renderComponent(type, props, writer, isSvg) {
715
693
  savedIdTree = pushTreeContext(1, 0);
716
694
  }
717
695
  const finish = () => {
718
- if (savedIdTree !== void 0)
719
- popTreeContext(savedIdTree);
696
+ if (savedIdTree !== void 0) popTreeContext(savedIdTree);
720
697
  popComponentScope(savedScope);
721
- if (isProvider)
722
- popContextValue(ctx, prevCtxValue);
698
+ if (isProvider) popContextValue(ctx, prevCtxValue);
723
699
  };
724
700
  if (result instanceof Promise) {
725
701
  const m = captureMap();
@@ -730,11 +706,9 @@ function renderComponent(type, props, writer, isSvg) {
730
706
  asyncSavedIdTree = pushTreeContext(1, 0);
731
707
  }
732
708
  const asyncFinish = () => {
733
- if (asyncSavedIdTree !== void 0)
734
- popTreeContext(asyncSavedIdTree);
709
+ if (asyncSavedIdTree !== void 0) popTreeContext(asyncSavedIdTree);
735
710
  popComponentScope(savedScope);
736
- if (isProvider)
737
- popContextValue(ctx, prevCtxValue);
711
+ if (isProvider) popContextValue(ctx, prevCtxValue);
738
712
  };
739
713
  const r2 = renderNode(resolved, writer, isSvg);
740
714
  if (r2 && typeof r2.then === "function") {
@@ -817,8 +791,7 @@ function renderChildArrayFrom(children, startIndex, writer, isSvg) {
817
791
  }
818
792
  }
819
793
  function renderChildren(children, writer, isSvg = false) {
820
- if (children == null)
821
- return;
794
+ if (children == null) return;
822
795
  if (Array.isArray(children)) {
823
796
  return renderChildArray(children, writer, isSvg);
824
797
  }
@@ -914,14 +887,12 @@ async function renderToString(element, options) {
914
887
  var { ssrBundlePath } = workerData;
915
888
  var _ssrMod = null;
916
889
  async function init() {
917
- if (_ssrMod)
918
- return;
890
+ if (_ssrMod) return;
919
891
  _ssrMod = await import(pathToFileURL(ssrBundlePath).href);
920
892
  }
921
893
  function deserializeRequest(s2) {
922
894
  const init2 = { method: s2.method, headers: new Headers(s2.headers) };
923
- if (s2.body)
924
- init2.body = s2.body.buffer;
895
+ if (s2.body) init2.body = s2.body.buffer;
925
896
  const req = new Request(s2.url, init2);
926
897
  Object.assign(req, { pathname: s2.pathname, search: s2.search, location: s2.location, cookies: s2.cookies });
927
898
  return req;
@@ -940,17 +911,14 @@ function renderHeadTag(tag, id, opts, selfClose = false) {
940
911
  let a = ` id="${escAttr(id)}"`;
941
912
  let inner = "";
942
913
  for (const [k, v] of Object.entries(opts)) {
943
- if (k === "key" || k === "children")
944
- continue;
914
+ if (k === "key" || k === "children") continue;
945
915
  if (k === "dangerouslySetInnerHTML") {
946
916
  inner = v.__html ?? "";
947
917
  continue;
948
918
  }
949
919
  const attr = HEAD_ATTR[k] ?? k;
950
- if (v === true)
951
- a += ` ${attr}`;
952
- else if (v !== false && v != null)
953
- a += ` ${attr}="${escAttr(String(v))}"`;
920
+ if (v === true) a += ` ${attr}`;
921
+ else if (v !== false && v != null) a += ` ${attr}="${escAttr(String(v))}"`;
954
922
  }
955
923
  return selfClose ? `<${tag}${a}>` : `<${tag}${a}>${inner}</${tag}>`;
956
924
  }
@@ -996,8 +964,7 @@ async function runFullLifecycle(serialReq) {
996
964
  const { context: _ctx, ...restProps } = getFinalProps ? await getFinalProps(props) : props;
997
965
  const serverData = {};
998
966
  for (const [key, entry] of unsuspend.cache) {
999
- if (entry.status === "fulfilled")
1000
- serverData[key] = entry.value;
967
+ if (entry.status === "fulfilled") serverData[key] = entry.value;
1001
968
  }
1002
969
  const clientProps = {
1003
970
  ...restProps,
@@ -1020,8 +987,7 @@ parentPort.on("message", async (msg) => {
1020
987
  const { id, type, request } = msg;
1021
988
  try {
1022
989
  await init();
1023
- if (type !== "renderFull")
1024
- return;
990
+ if (type !== "renderFull") return;
1025
991
  const { html, headHtml, status } = await runFullLifecycle(request);
1026
992
  parentPort.postMessage({ id, html, headHtml, status });
1027
993
  } catch (err) {
package/dist/ssr-watch.js CHANGED
@@ -194,8 +194,7 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
194
194
  if (!isServerBuild && opts.reactMode !== void 0) {
195
195
  const rules = localConfig.module?.rules ?? [];
196
196
  for (const rule of rules) {
197
- if (!rule?.use || !Array.isArray(rule.use))
198
- continue;
197
+ if (!rule?.use || !Array.isArray(rule.use)) continue;
199
198
  for (const entry3 of rule.use) {
200
199
  if (entry3?.loader?.includes("swc-loader")) {
201
200
  entry3.options = entry3.options ?? {};
@@ -209,6 +208,25 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
209
208
  }
210
209
  }
211
210
  const extraPlugins = [];
211
+ if (!isServerBuild) {
212
+ extraPlugins.push({
213
+ apply(compiler) {
214
+ compiler.hooks.compilation.tap("HadarsWorkerChunkLoading", (compilation) => {
215
+ compilation.hooks.childCompiler.tap(
216
+ "HadarsWorkerChunkLoading",
217
+ (childCompiler) => {
218
+ if (childCompiler.options?.output) {
219
+ childCompiler.options.output.chunkLoading = "import-scripts";
220
+ }
221
+ if (childCompiler.options?.experiments) {
222
+ childCompiler.options.experiments.outputModule = false;
223
+ }
224
+ }
225
+ );
226
+ });
227
+ }
228
+ });
229
+ }
212
230
  const defineValues = { ...opts.define ?? {} };
213
231
  if (!isServerBuild && opts.reactMode !== void 0) {
214
232
  defineValues["process.env.NODE_ENV"] = JSON.stringify(opts.reactMode);
@@ -285,7 +303,8 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
285
303
  },
286
304
  isDev && !isServerBuild && new ReactRefreshPlugin(),
287
305
  includeHotPlugin && isDev && !isServerBuild && new rspack.HotModuleReplacementPlugin(),
288
- ...extraPlugins
306
+ ...extraPlugins,
307
+ ...opts.plugins ?? []
289
308
  ],
290
309
  ...localConfig,
291
310
  // Merge base resolve (modules, tsConfig, extensions) with per-build resolve
@@ -361,11 +380,9 @@ var compileEntry = async (entry2, opts) => {
361
380
  var args = process.argv.slice(2);
362
381
  var argv = {};
363
382
  for (const a of args) {
364
- if (!a.startsWith("--"))
365
- continue;
383
+ if (!a.startsWith("--")) continue;
366
384
  const idx = a.indexOf("=");
367
- if (idx === -1)
368
- continue;
385
+ if (idx === -1) continue;
369
386
  const key = a.slice(2, idx);
370
387
  const val = a.slice(idx + 1);
371
388
  argv[key] = val;
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import type { AppContext, AppUnsuspend, LinkProps, MetaProps, ScriptProps, StyleProps } from '../types/hadars'
2
+ import type { AppContext as HadarsAppContext, AppUnsuspend, LinkProps, MetaProps, ScriptProps, StyleProps } from '../types/hadars'
3
3
 
4
4
  interface InnerContext {
5
5
  setTitle: (title: string) => void;
@@ -31,7 +31,7 @@ const AppContext = React.createContext<InnerContext>({
31
31
 
32
32
  export const AppProviderSSR: React.FC<{
33
33
  children: React.ReactNode,
34
- context: AppContext,
34
+ context: HadarsAppContext,
35
35
  }> = React.memo( ({ children, context }) => {
36
36
 
37
37
  const { head } = context;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hadars",
3
- "version": "0.1.35",
3
+ "version": "0.1.37",
4
4
  "description": "Minimal SSR framework for React — rspack, HMR, TypeScript, Bun/Node/Deno",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -19,17 +19,22 @@
19
19
  ],
20
20
  "exports": {
21
21
  ".": {
22
+ "types": "./dist/index.d.ts",
22
23
  "import": "./dist/index.js",
23
- "require": "./dist/index.cjs",
24
- "types": "./dist/index.d.ts"
24
+ "require": "./dist/index.cjs"
25
25
  },
26
26
  "./cli": {
27
27
  "import": "./dist/cli.js",
28
28
  "require": "./dist/cli.js"
29
+ },
30
+ "./lambda": {
31
+ "types": "./dist/lambda.d.ts",
32
+ "import": "./dist/lambda.js",
33
+ "require": "./dist/lambda.cjs"
29
34
  }
30
35
  },
31
36
  "scripts": {
32
- "build:lib": "tsup src/index.tsx src/slim-react/index.ts src/slim-react/jsx-runtime.ts --format esm,cjs --dts --out-dir dist --clean --external '@rspack/*' --external '@rspack/binding'",
37
+ "build:lib": "tsup src/index.tsx src/lambda.ts src/slim-react/index.ts src/slim-react/jsx-runtime.ts --format esm,cjs --dts --out-dir dist --clean --external '@rspack/*' --external '@rspack/binding'",
33
38
  "build:cli": "node build-scripts/build-cli.mjs",
34
39
  "build:all": "npm run build:lib && npm run build:cli",
35
40
  "test": "bun test test/render-compare.test.tsx && bun test test/ssr.test.ts",
@@ -72,14 +77,14 @@
72
77
  "devDependencies": {
73
78
  "@types/react": "^19.2.14",
74
79
  "@types/react-dom": "^19.2.3",
75
- "esbuild": "^0.19.12",
76
80
  "react": "^19.2.4",
77
81
  "react-dom": "^19.2.4",
78
- "tsup": "^6.7.0",
82
+ "tsup": "^8.3.5",
79
83
  "typescript": "^5.9.3"
80
84
  },
81
85
  "dependencies": {
82
86
  "@svgr/webpack": "^8.1.0",
87
+ "esbuild": "^0.27.4",
83
88
  "postcss": "^8.5.8",
84
89
  "postcss-load-config": "^6.0.1",
85
90
  "postcss-loader": "^8.2.1"
package/src/build.ts CHANGED
@@ -16,7 +16,11 @@ import os from 'node:os';
16
16
  import { spawn } from 'node:child_process';
17
17
  import cluster from 'node:cluster';
18
18
  import type { HadarsEntryModule, HadarsOptions, HadarsProps } from "./types/hadars";
19
- const encoder = new TextEncoder();
19
+ import {
20
+ HEAD_MARKER, BODY_MARKER,
21
+ buildSsrResponse, makePrecontentHtmlGetter,
22
+ CacheFetchHandler, createRenderCache,
23
+ } from './utils/ssrHandler';
20
24
 
21
25
  /**
22
26
  * Reads an HTML template, processes any `<style>` blocks through PostCSS
@@ -69,11 +73,6 @@ async function processHtmlTemplate(templatePath: string): Promise<string> {
69
73
  return cachedPath;
70
74
  }
71
75
 
72
- const HEAD_MARKER = '<meta name="HADARS_HEAD">';
73
- const BODY_MARKER = '<meta name="HADARS_BODY">';
74
-
75
-
76
-
77
76
  // Round-robin thread pool for SSR rendering — used on Bun/Deno where
78
77
  // node:cluster is not available but node:worker_threads is.
79
78
 
@@ -192,34 +191,6 @@ class RenderWorkerPool {
192
191
  }
193
192
  }
194
193
 
195
- async function buildSsrResponse(
196
- bodyHtml: string,
197
- clientProps: Record<string, unknown>,
198
- headHtml: string,
199
- status: number,
200
- getPrecontentHtml: (headHtml: string) => Promise<[string, string]>,
201
- ): Promise<Response> {
202
- const responseStream = new ReadableStream({
203
- async start(controller) {
204
- const [precontentHtml, postContent] = await getPrecontentHtml(headHtml);
205
- // Flush the shell (precontentHtml) immediately so the browser can
206
- // start loading CSS/fonts before the body is assembled.
207
- controller.enqueue(encoder.encode(precontentHtml));
208
-
209
- const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, '\\u003c');
210
- controller.enqueue(encoder.encode(
211
- `<div id="app">${bodyHtml}</div><script id="hadars" type="application/json">${scriptContent}</script>` + postContent
212
- ));
213
- controller.close();
214
- },
215
- });
216
-
217
- return new Response(responseStream, {
218
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
219
- status,
220
- });
221
- }
222
-
223
194
  /** Serialize a HadarsRequest into a structure-clonable object for postMessage. */
224
195
  async function serializeRequest(req: any): Promise<SerializableRequest> {
225
196
  const isGetOrHead = ['GET', 'HEAD'].includes(req.method ?? 'GET');
@@ -238,138 +209,6 @@ async function serializeRequest(req: any): Promise<SerializableRequest> {
238
209
  };
239
210
  }
240
211
 
241
- /**
242
- * Returns a function that parses `out.html` into pre-head, post-head, and
243
- * post-content segments and caches the result. Call the returned function with
244
- * the per-request headHtml to produce the full HTML prefix and suffix.
245
- */
246
- const makePrecontentHtmlGetter = (htmlFilePromise: Promise<string>) => {
247
- let preHead: string | null = null;
248
- let postHead: string | null = null;
249
- let postContent: string | null = null;
250
- return async (headHtml: string): Promise<[string, string]> => {
251
- if (preHead === null || postHead === null || postContent === null) {
252
- const html = await htmlFilePromise;
253
- const headEnd = html.indexOf(HEAD_MARKER);
254
- const contentStart = html.indexOf(BODY_MARKER);
255
- preHead = html.slice(0, headEnd);
256
- postHead = html.slice(headEnd + HEAD_MARKER.length, contentStart);
257
- postContent = html.slice(contentStart + BODY_MARKER.length);
258
- }
259
- return [preHead! + headHtml + postHead!, postContent!];
260
- };
261
- };
262
-
263
- // ── SSR response cache ────────────────────────────────────────────────────────
264
-
265
- interface CacheEntry {
266
- /** Gzip-compressed response body — much cheaper to keep in RAM than raw HTML. */
267
- body: Uint8Array;
268
- status: number;
269
- /** Headers with Content-Encoding: gzip already set. */
270
- headers: [string, string][];
271
- expiresAt: number | null;
272
- }
273
-
274
- type CacheFetchHandler = (req: Request, ctx: any) => Promise<Response | undefined>;
275
-
276
- async function transformStream(data: Uint8Array, stream: { writable: WritableStream; readable: ReadableStream<Uint8Array> }): Promise<Uint8Array> {
277
- const writer = stream.writable.getWriter();
278
- writer.write(data);
279
- writer.close();
280
- const chunks: Uint8Array[] = [];
281
- const reader = stream.readable.getReader();
282
- while (true) {
283
- const { done, value } = await reader.read();
284
- if (done) break;
285
- chunks.push(value);
286
- }
287
- const total = chunks.reduce((n, c) => n + c.length, 0);
288
- const out = new Uint8Array(total);
289
- let offset = 0;
290
- for (const c of chunks) { out.set(c, offset); offset += c.length; }
291
- return out;
292
- }
293
-
294
- const gzipCompress = (d: Uint8Array) => transformStream(d, new (globalThis as any).CompressionStream('gzip'));
295
- const gzipDecompress = (d: Uint8Array) => transformStream(d, new (globalThis as any).DecompressionStream('gzip'));
296
-
297
- async function buildCacheEntry(res: Response, ttl: number | undefined): Promise<CacheEntry> {
298
- const buf = await res.arrayBuffer();
299
- const body = await gzipCompress(new Uint8Array(buf));
300
- const headers: [string, string][] = [];
301
- res.headers.forEach((v, k) => {
302
- if (k.toLowerCase() !== 'content-encoding' && k.toLowerCase() !== 'content-length') {
303
- headers.push([k, v]);
304
- }
305
- });
306
- headers.push(['content-encoding', 'gzip']);
307
- return { body, status: res.status, headers, expiresAt: ttl != null ? Date.now() + ttl : null };
308
- }
309
-
310
- async function serveFromEntry(entry: CacheEntry, req: Request): Promise<Response> {
311
- const accept = req.headers.get('Accept-Encoding') ?? '';
312
- if (accept.includes('gzip')) {
313
- return new Response(entry.body.buffer as ArrayBuffer, { status: entry.status, headers: entry.headers });
314
- }
315
- // Client doesn't support gzip — decompress before serving
316
- const plain = await gzipDecompress(entry.body);
317
- const headers = entry.headers.filter(([k]) => k.toLowerCase() !== 'content-encoding');
318
- return new Response(plain.buffer as ArrayBuffer, { status: entry.status, headers });
319
- }
320
-
321
- function createRenderCache(
322
- opts: NonNullable<HadarsOptions['cache']>,
323
- handler: CacheFetchHandler,
324
- ): CacheFetchHandler {
325
- const store = new Map<string, CacheEntry>();
326
- // Single-flight map: coalesces concurrent misses for the same key onto one render.
327
- const inFlight = new Map<string, Promise<CacheEntry | null>>();
328
-
329
- return async (req, ctx) => {
330
- const hadarsReq = parseRequest(req);
331
- const cacheOpts = await opts(hadarsReq);
332
- const key = cacheOpts?.key ?? null;
333
-
334
- if (key != null) {
335
- const entry = store.get(key);
336
- if (entry) {
337
- const expired = entry.expiresAt != null && Date.now() >= entry.expiresAt;
338
- if (!expired) {
339
- return serveFromEntry(entry, req);
340
- }
341
- store.delete(key);
342
- }
343
-
344
- // Single-flight: if a render for this key is already in progress, await
345
- // it instead of starting a duplicate render (thundering herd prevention).
346
- let flight = inFlight.get(key);
347
- if (!flight) {
348
- const ttl = cacheOpts?.ttl;
349
- flight = handler(new Request(req), ctx)
350
- .then(async (res) => {
351
- if (!res || res.status < 200 || res.status >= 300 || res.headers.has('set-cookie')) {
352
- return null;
353
- }
354
- const newEntry = await buildCacheEntry(res, ttl);
355
- store.set(key, newEntry);
356
- return newEntry;
357
- })
358
- .catch(() => null)
359
- .finally(() => inFlight.delete(key));
360
- inFlight.set(key, flight);
361
- }
362
-
363
- const newEntry = await flight;
364
- if (newEntry) return serveFromEntry(newEntry, req);
365
- // Render was uncacheable (error, Set-Cookie, etc.) — fall through to a
366
- // fresh independent render for this request so it still gets a response.
367
- }
368
-
369
- return handler(req, ctx);
370
- };
371
- }
372
-
373
212
  interface HadarsRuntimeOptions extends HadarsOptions {
374
213
  mode: "development" | "production";
375
214
  }
@@ -502,6 +341,7 @@ export const dev = async (options: HadarsRuntimeOptions) => {
502
341
  swcPlugins: options.swcPlugins,
503
342
  define: options.define,
504
343
  moduleRules: options.moduleRules,
344
+ plugins: options.plugins,
505
345
  reactMode: options.reactMode,
506
346
  htmlTemplate: resolvedHtmlTemplate,
507
347
  });
@@ -757,6 +597,7 @@ export const build = async (options: HadarsRuntimeOptions) => {
757
597
  swcPlugins: options.swcPlugins,
758
598
  define: options.define,
759
599
  moduleRules: options.moduleRules,
600
+ plugins: options.plugins,
760
601
  optimization: options.optimization,
761
602
  reactMode: options.reactMode,
762
603
  htmlTemplate: resolvedHtmlTemplate,
@@ -775,6 +616,7 @@ export const build = async (options: HadarsRuntimeOptions) => {
775
616
  swcPlugins: options.swcPlugins,
776
617
  define: options.define,
777
618
  moduleRules: options.moduleRules,
619
+ plugins: options.plugins,
778
620
  }),
779
621
  ]);
780
622
  await fs.rm(tmpFilePath);