hadars 0.1.11 → 0.1.12

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 ninety contributors
3
+ Copyright (c) 2025 hadars contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/cli-lib.ts CHANGED
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'
2
2
  import { mkdir, writeFile } from 'node:fs/promises'
3
3
  import { resolve, join } from 'node:path'
4
4
  import * as Hadars from './src/build'
5
- import type { HadarsOptions } from './src/types/ninety'
5
+ import type { HadarsOptions } from './src/types/hadars'
6
6
 
7
7
  const SUPPORTED = ['hadars.config.js', 'hadars.config.mjs', 'hadars.config.cjs', 'hadars.config.ts']
8
8
 
package/cli.ts CHANGED
@@ -7,12 +7,12 @@ import { runCli } from './cli-lib'
7
7
  // (native Bun.serve, WebSocket support, etc.).
8
8
  // Falls back to Node.js silently if bun is not in PATH.
9
9
  if (typeof (globalThis as any).Bun === 'undefined' && typeof (globalThis as any).Deno === 'undefined') {
10
- const child = spawn('bun', [process.argv[1], ...process.argv.slice(2)], {
10
+ const child = spawn('bun', [process.argv[1]!, ...process.argv.slice(2)], {
11
11
  stdio: 'inherit',
12
12
  env: process.env,
13
13
  });
14
14
  const sigs = ['SIGINT', 'SIGTERM', 'SIGHUP'] as const;
15
- const fwd = (sig: string) => () => { try { child.kill(sig); } catch {} };
15
+ const fwd = (sig: NodeJS.Signals) => () => { try { child.kill(sig); } catch {} };
16
16
  for (const sig of sigs) process.on(sig, fwd(sig));
17
17
  child.on('error', (err: any) => {
18
18
  for (const sig of sigs) process.removeAllListeners(sig);
package/dist/cli.js CHANGED
@@ -150,8 +150,8 @@ async function getStaticMarkupRenderer() {
150
150
  return _renderToStaticMarkup;
151
151
  }
152
152
  var ESC = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" };
153
- var escAttr = (s) => s.replace(/[&<>"]/g, (c) => ESC[c]);
154
- var escText = (s) => s.replace(/[&<>]/g, (c) => ESC[c]);
153
+ var escAttr = (s) => s.replace(/[&<>"]/g, (c) => ESC[c] ?? c);
154
+ var escText = (s) => s.replace(/[&<>]/g, (c) => ESC[c] ?? c);
155
155
  var ATTR = {
156
156
  className: "class",
157
157
  htmlFor: "for",
@@ -766,7 +766,7 @@ async function tryServeFile(filePath) {
766
766
  const data = await readFile(filePath);
767
767
  const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
768
768
  const contentType = MIME[ext] ?? "application/octet-stream";
769
- return new Response(data, { headers: { "Content-Type": contentType } });
769
+ return new Response(data.buffer, { headers: { "Content-Type": contentType } });
770
770
  } catch {
771
771
  return null;
772
772
  }
@@ -784,8 +784,8 @@ 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
- var HEAD_MARKER = '<meta name="NINETY_HEAD">';
788
- var BODY_MARKER = '<meta name="NINETY_BODY">';
787
+ var HEAD_MARKER = '<meta name="HADARS_HEAD">';
788
+ var BODY_MARKER = '<meta name="HADARS_BODY">';
789
789
  var _renderToString = null;
790
790
  async function getRenderToString() {
791
791
  if (!_renderToString) {
@@ -979,8 +979,30 @@ async function transformStream(data, stream) {
979
979
  }
980
980
  var gzipCompress = (d) => transformStream(d, new globalThis.CompressionStream("gzip"));
981
981
  var gzipDecompress = (d) => transformStream(d, new globalThis.DecompressionStream("gzip"));
982
+ async function buildCacheEntry(res, ttl) {
983
+ const buf = await res.arrayBuffer();
984
+ const body = await gzipCompress(new Uint8Array(buf));
985
+ const headers = [];
986
+ res.headers.forEach((v, k) => {
987
+ if (k.toLowerCase() !== "content-encoding" && k.toLowerCase() !== "content-length") {
988
+ headers.push([k, v]);
989
+ }
990
+ });
991
+ headers.push(["content-encoding", "gzip"]);
992
+ return { body, status: res.status, headers, expiresAt: ttl != null ? Date.now() + ttl : null };
993
+ }
994
+ async function serveFromEntry(entry, req) {
995
+ const accept = req.headers.get("Accept-Encoding") ?? "";
996
+ if (accept.includes("gzip")) {
997
+ return new Response(entry.body.buffer, { status: entry.status, headers: entry.headers });
998
+ }
999
+ const plain = await gzipDecompress(entry.body);
1000
+ const headers = entry.headers.filter(([k]) => k.toLowerCase() !== "content-encoding");
1001
+ return new Response(plain.buffer, { status: entry.status, headers });
1002
+ }
982
1003
  function createRenderCache(opts, handler) {
983
1004
  const store = /* @__PURE__ */ new Map();
1005
+ const inFlight = /* @__PURE__ */ new Map();
984
1006
  return async (req, ctx) => {
985
1007
  const hadarsReq = parseRequest(req);
986
1008
  const cacheOpts = await opts(hadarsReq);
@@ -988,40 +1010,30 @@ function createRenderCache(opts, handler) {
988
1010
  if (key != null) {
989
1011
  const entry = store.get(key);
990
1012
  if (entry) {
991
- if (entry.expiresAt == null || Date.now() < entry.expiresAt) {
992
- const accept = req.headers.get("Accept-Encoding") ?? "";
993
- if (accept.includes("gzip")) {
994
- return new Response(entry.body.buffer, { status: entry.status, headers: entry.headers });
995
- }
996
- const plain = await gzipDecompress(entry.body);
997
- const headers = entry.headers.filter(([k]) => k.toLowerCase() !== "content-encoding");
998
- return new Response(plain.buffer, { status: entry.status, headers });
1013
+ const expired = entry.expiresAt != null && Date.now() >= entry.expiresAt;
1014
+ if (!expired) {
1015
+ return serveFromEntry(entry, req);
999
1016
  }
1000
1017
  store.delete(key);
1001
1018
  }
1002
- }
1003
- const res = await handler(req, ctx);
1004
- if (key != null && res) {
1005
- const ttl = cacheOpts?.ttl;
1006
- res.clone().arrayBuffer().then(async (buf) => {
1007
- const body = await gzipCompress(new Uint8Array(buf));
1008
- const headers = [];
1009
- res.headers.forEach((v, k) => {
1010
- if (k.toLowerCase() !== "content-encoding" && k.toLowerCase() !== "content-length") {
1011
- headers.push([k, v]);
1019
+ let flight = inFlight.get(key);
1020
+ if (!flight) {
1021
+ const ttl = cacheOpts?.ttl;
1022
+ flight = handler(new Request(req), ctx).then(async (res) => {
1023
+ if (!res || res.status < 200 || res.status >= 300 || res.headers.has("set-cookie")) {
1024
+ return null;
1012
1025
  }
1013
- });
1014
- headers.push(["content-encoding", "gzip"]);
1015
- store.set(key, {
1016
- body,
1017
- status: res.status,
1018
- headers,
1019
- expiresAt: ttl != null ? Date.now() + ttl : null
1020
- });
1021
- }).catch(() => {
1022
- });
1026
+ const newEntry2 = await buildCacheEntry(res, ttl);
1027
+ store.set(key, newEntry2);
1028
+ return newEntry2;
1029
+ }).catch(() => null).finally(() => inFlight.delete(key));
1030
+ inFlight.set(key, flight);
1031
+ }
1032
+ const newEntry = await flight;
1033
+ if (newEntry)
1034
+ return serveFromEntry(newEntry, req);
1023
1035
  }
1024
- return res;
1036
+ return handler(req, ctx);
1025
1037
  };
1026
1038
  }
1027
1039
  var SSR_FILENAME = "index.ssr.js";
@@ -1112,11 +1124,11 @@ var dev = async (options) => {
1112
1124
  allowedHosts: "all"
1113
1125
  }, clientCompiler);
1114
1126
  console.log(`Starting HMR dev server on port ${hmrPort}`);
1115
- await new Promise((resolve2, reject) => {
1116
- let resolved = false;
1127
+ let clientResolved = false;
1128
+ const clientBuildDone = new Promise((resolve2, reject) => {
1117
1129
  clientCompiler.hooks.done.tap("initial-build", (stats) => {
1118
- if (!resolved) {
1119
- resolved = true;
1130
+ if (!clientResolved) {
1131
+ clientResolved = true;
1120
1132
  console.log(stats.toString({ colors: true }));
1121
1133
  resolve2();
1122
1134
  }
@@ -1156,37 +1168,40 @@ var dev = async (options) => {
1156
1168
  const marker = "ssr-watch: initial-build-complete";
1157
1169
  const rebuildMarker = "ssr-watch: SSR rebuilt";
1158
1170
  const decoder = new TextDecoder();
1159
- let gotMarker = false;
1160
1171
  let stdoutReader = null;
1161
- try {
1162
- stdoutReader = stdoutWebStream.getReader();
1163
- let buf = "";
1164
- const start = Date.now();
1165
- const timeoutMs = 2e4;
1166
- while (Date.now() - start < timeoutMs) {
1167
- const { done, value } = await stdoutReader.read();
1168
- if (done) {
1169
- stdoutReader = null;
1170
- break;
1171
- }
1172
- const chunk = decoder.decode(value, { stream: true });
1173
- buf += chunk;
1174
- try {
1175
- process.stdout.write(chunk);
1176
- } catch (e) {
1172
+ const ssrBuildDone = (async () => {
1173
+ let gotMarker = false;
1174
+ try {
1175
+ stdoutReader = stdoutWebStream.getReader();
1176
+ let buf = "";
1177
+ const start = Date.now();
1178
+ const timeoutMs = 2e4;
1179
+ while (Date.now() - start < timeoutMs) {
1180
+ const { done, value } = await stdoutReader.read();
1181
+ if (done) {
1182
+ stdoutReader = null;
1183
+ break;
1184
+ }
1185
+ const chunk = decoder.decode(value, { stream: true });
1186
+ buf += chunk;
1187
+ try {
1188
+ process.stdout.write(chunk);
1189
+ } catch (e) {
1190
+ }
1191
+ if (buf.includes(marker)) {
1192
+ gotMarker = true;
1193
+ break;
1194
+ }
1177
1195
  }
1178
- if (buf.includes(marker)) {
1179
- gotMarker = true;
1180
- break;
1196
+ if (!gotMarker) {
1197
+ console.warn("SSR worker did not signal initial build completion within timeout");
1181
1198
  }
1199
+ } catch (err) {
1200
+ console.error("Error reading SSR worker output", err);
1201
+ stdoutReader = null;
1182
1202
  }
1183
- if (!gotMarker) {
1184
- console.warn("SSR worker did not signal initial build completion within timeout");
1185
- }
1186
- } catch (err) {
1187
- console.error("Error reading SSR worker output", err);
1188
- stdoutReader = null;
1189
- }
1203
+ })();
1204
+ await Promise.all([clientBuildDone, ssrBuildDone]);
1190
1205
  if (stdoutReader) {
1191
1206
  const reader = stdoutReader;
1192
1207
  (async () => {
@@ -11,7 +11,7 @@ var _ssrMod = null;
11
11
  async function init() {
12
12
  if (_React && _ssrMod)
13
13
  return;
14
- const req = createRequire(pathMod.resolve(process.cwd(), "__ninety_fake__.js"));
14
+ const req = createRequire(pathMod.resolve(process.cwd(), "__hadars_fake__.js"));
15
15
  if (!_React) {
16
16
  const reactPath = pathToFileURL(req.resolve("react")).href;
17
17
  const reactMod = await import(reactPath);
@@ -30,7 +30,7 @@ async function init() {
30
30
  function deserializeRequest(s) {
31
31
  const init2 = { method: s.method, headers: new Headers(s.headers) };
32
32
  if (s.body)
33
- init2.body = s.body;
33
+ init2.body = s.body.buffer;
34
34
  const req = new Request(s.url, init2);
35
35
  Object.assign(req, {
36
36
  pathname: s.pathname,
@@ -3,9 +3,9 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <meta name="NINETY_HEAD">
6
+ <meta name="HADARS_HEAD">
7
7
  </head>
8
8
  <body>
9
- <meta name="NINETY_BODY">
9
+ <meta name="HADARS_BODY">
10
10
  </body>
11
11
  </html>
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import type { AppContext, AppUnsuspend, LinkProps, MetaProps, ScriptProps, StyleProps } from '../types/ninety'
2
+ import type { AppContext, AppUnsuspend, LinkProps, MetaProps, ScriptProps, StyleProps } from '../types/hadars'
3
3
 
4
4
  interface InnerContext {
5
5
  setTitle: (title: string) => void;
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { hydrateRoot, createRoot } from 'react-dom/client';
3
- import type { HadarsEntryModule } from '../types/ninety';
3
+ import type { HadarsEntryModule } from '../types/hadars';
4
4
  import { initServerDataCache } from 'hadars';
5
5
  import * as _appMod from '$_MOD_PATH$';
6
6
 
package/index.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  // Bun entry point — re-exports the full public API from source so that
2
- // `ninety-bun` (which runs TypeScript directly) gets the same exports as
2
+ // hadars running TypeScript directly gets the same exports as
3
3
  // the compiled `dist/index.js` used by Node.js / Deno.
4
4
  export type {
5
- NinetyOptions,
6
- NinetyProps,
7
- NinetyRequest,
8
- NinetyGetAfterRenderProps,
9
- NinetyGetFinalProps,
10
- NinetyGetInitialProps,
11
- NinetyGetClientProps,
12
- NinetyEntryModule,
13
- NinetyApp,
14
- } from "./src/types/ninety";
15
- export { NinetyHead, NinetyContext, loadModule } from "./src/index";
5
+ HadarsOptions,
6
+ HadarsProps,
7
+ HadarsRequest,
8
+ HadarsGetAfterRenderProps,
9
+ HadarsGetFinalProps,
10
+ HadarsGetInitialProps,
11
+ HadarsGetClientProps,
12
+ HadarsEntryModule,
13
+ HadarsApp,
14
+ } from "./src/types/hadars";
15
+ export { HadarsHead, HadarsContext, loadModule } from "./src/index";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hadars",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Minimal SSR framework for React — rspack, HMR, TypeScript, Bun/Node/Deno",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -67,24 +67,20 @@
67
67
  "@rspack/plugin-react-refresh": "^1.6.1",
68
68
  "@swc/core": "^1.15.18",
69
69
  "@types/bun": "latest",
70
- "@types/react-dom": "^19.2.3",
71
- "react-refresh": "^0.17.0",
72
- "typescript": "^5.9.3"
70
+ "react-refresh": "^0.17.0"
73
71
  },
74
72
  "devDependencies": {
75
73
  "@types/react": "^19.2.14",
76
- "@types/react-dom": "^19.0.0",
74
+ "@types/react-dom": "^19.2.3",
77
75
  "esbuild": "^0.19.12",
78
- "tsup": "^6.7.0"
76
+ "tsup": "^6.7.0",
77
+ "typescript": "^5.9.3"
79
78
  },
80
79
  "dependencies": {
81
80
  "@mdx-js/loader": "^3.1.1",
82
- "@mdx-js/react": "^3.1.1",
83
81
  "@svgr/webpack": "^8.1.0",
84
- "@tailwindcss/postcss": "^4.2.1",
85
82
  "postcss": "^8.5.8",
86
- "postcss-loader": "^8.2.1",
87
- "tailwindcss": "^4.2.1"
83
+ "postcss-loader": "^8.2.1"
88
84
  },
89
85
  "license": "MIT",
90
86
  "repository": {
package/src/build.ts CHANGED
@@ -16,11 +16,11 @@ import { existsSync } from 'node:fs';
16
16
  import os from 'node:os';
17
17
  import { spawn } from 'node:child_process';
18
18
  import cluster from 'node:cluster';
19
- import type { HadarsEntryModule, HadarsOptions, HadarsProps } from "./types/ninety";
19
+ import type { HadarsEntryModule, HadarsOptions, HadarsProps } from "./types/hadars";
20
20
  const encoder = new TextEncoder();
21
21
 
22
- const HEAD_MARKER = '<meta name="NINETY_HEAD">';
23
- const BODY_MARKER = '<meta name="NINETY_BODY">';
22
+ const HEAD_MARKER = '<meta name="HADARS_HEAD">';
23
+ const BODY_MARKER = '<meta name="HADARS_BODY">';
24
24
 
25
25
  // Resolve renderToString from react-dom/server in the project's node_modules.
26
26
  let _renderToString: ((element: any) => string) | null = null;
@@ -264,11 +264,37 @@ async function transformStream(data: Uint8Array, stream: { writable: WritableStr
264
264
  const gzipCompress = (d: Uint8Array) => transformStream(d, new (globalThis as any).CompressionStream('gzip'));
265
265
  const gzipDecompress = (d: Uint8Array) => transformStream(d, new (globalThis as any).DecompressionStream('gzip'));
266
266
 
267
+ async function buildCacheEntry(res: Response, ttl: number | undefined): Promise<CacheEntry> {
268
+ const buf = await res.arrayBuffer();
269
+ const body = await gzipCompress(new Uint8Array(buf));
270
+ const headers: [string, string][] = [];
271
+ res.headers.forEach((v, k) => {
272
+ if (k.toLowerCase() !== 'content-encoding' && k.toLowerCase() !== 'content-length') {
273
+ headers.push([k, v]);
274
+ }
275
+ });
276
+ headers.push(['content-encoding', 'gzip']);
277
+ return { body, status: res.status, headers, expiresAt: ttl != null ? Date.now() + ttl : null };
278
+ }
279
+
280
+ async function serveFromEntry(entry: CacheEntry, req: Request): Promise<Response> {
281
+ const accept = req.headers.get('Accept-Encoding') ?? '';
282
+ if (accept.includes('gzip')) {
283
+ return new Response(entry.body.buffer as ArrayBuffer, { status: entry.status, headers: entry.headers });
284
+ }
285
+ // Client doesn't support gzip — decompress before serving
286
+ const plain = await gzipDecompress(entry.body);
287
+ const headers = entry.headers.filter(([k]) => k.toLowerCase() !== 'content-encoding');
288
+ return new Response(plain.buffer as ArrayBuffer, { status: entry.status, headers });
289
+ }
290
+
267
291
  function createRenderCache(
268
292
  opts: NonNullable<HadarsOptions['cache']>,
269
293
  handler: CacheFetchHandler,
270
294
  ): CacheFetchHandler {
271
- const store = new Map<string, CacheEntry>();
295
+ const store = new Map<string, CacheEntry>();
296
+ // Single-flight map: coalesces concurrent misses for the same key onto one render.
297
+ const inFlight = new Map<string, Promise<CacheEntry | null>>();
272
298
 
273
299
  return async (req, ctx) => {
274
300
  const hadarsReq = parseRequest(req);
@@ -278,45 +304,39 @@ function createRenderCache(
278
304
  if (key != null) {
279
305
  const entry = store.get(key);
280
306
  if (entry) {
281
- if (entry.expiresAt == null || Date.now() < entry.expiresAt) {
282
- const accept = req.headers.get('Accept-Encoding') ?? '';
283
- if (accept.includes('gzip')) {
284
- return new Response(entry.body.buffer as ArrayBuffer, { status: entry.status, headers: entry.headers });
285
- }
286
- // Client doesn't support gzip — decompress before serving
287
- const plain = await gzipDecompress(entry.body);
288
- const headers = entry.headers.filter(([k]) => k.toLowerCase() !== 'content-encoding');
289
- return new Response(plain.buffer as ArrayBuffer, { status: entry.status, headers });
307
+ const expired = entry.expiresAt != null && Date.now() >= entry.expiresAt;
308
+ if (!expired) {
309
+ return serveFromEntry(entry, req);
290
310
  }
291
311
  store.delete(key);
292
312
  }
293
- }
294
313
 
295
- const res = await handler(req, ctx);
296
-
297
- // Compress and cache in the background via clone() so the original
298
- // stream is returned to the client immediately.
299
- if (key != null && res) {
300
- const ttl = cacheOpts?.ttl;
301
- res.clone().arrayBuffer().then(async (buf) => {
302
- const body = await gzipCompress(new Uint8Array(buf));
303
- const headers: [string, string][] = [];
304
- res.headers.forEach((v, k) => {
305
- if (k.toLowerCase() !== 'content-encoding' && k.toLowerCase() !== 'content-length') {
306
- headers.push([k, v]);
307
- }
308
- });
309
- headers.push(['content-encoding', 'gzip']);
310
- store.set(key, {
311
- body,
312
- status: res.status,
313
- headers,
314
- expiresAt: ttl != null ? Date.now() + ttl : null,
315
- });
316
- }).catch(() => { /* ignore read errors on clone */ });
314
+ // Single-flight: if a render for this key is already in progress, await
315
+ // it instead of starting a duplicate render (thundering herd prevention).
316
+ let flight = inFlight.get(key);
317
+ if (!flight) {
318
+ const ttl = cacheOpts?.ttl;
319
+ flight = handler(new Request(req), ctx)
320
+ .then(async (res) => {
321
+ if (!res || res.status < 200 || res.status >= 300 || res.headers.has('set-cookie')) {
322
+ return null;
323
+ }
324
+ const newEntry = await buildCacheEntry(res, ttl);
325
+ store.set(key, newEntry);
326
+ return newEntry;
327
+ })
328
+ .catch(() => null)
329
+ .finally(() => inFlight.delete(key));
330
+ inFlight.set(key, flight);
331
+ }
332
+
333
+ const newEntry = await flight;
334
+ if (newEntry) return serveFromEntry(newEntry, req);
335
+ // Render was uncacheable (error, Set-Cookie, etc.) fall through to a
336
+ // fresh independent render for this request so it still gets a response.
317
337
  }
318
338
 
319
- return res;
339
+ return handler(req, ctx);
320
340
  };
321
341
  }
322
342
 
@@ -457,11 +477,13 @@ export const dev = async (options: HadarsRuntimeOptions) => {
457
477
  }, clientCompiler as any);
458
478
 
459
479
  console.log(`Starting HMR dev server on port ${hmrPort}`);
460
- await new Promise<void>((resolve, reject) => {
461
- let resolved = false;
480
+
481
+ // Kick off client build — does NOT await here so SSR worker can start in parallel.
482
+ let clientResolved = false;
483
+ const clientBuildDone = new Promise<void>((resolve, reject) => {
462
484
  (clientCompiler as any).hooks.done.tap('initial-build', (stats: any) => {
463
- if (!resolved) {
464
- resolved = true;
485
+ if (!clientResolved) {
486
+ clientResolved = true;
465
487
  console.log(stats.toString({ colors: true }));
466
488
  resolve();
467
489
  }
@@ -472,6 +494,7 @@ export const dev = async (options: HadarsRuntimeOptions) => {
472
494
  // Start SSR watcher in a separate process to avoid creating two rspack
473
495
  // compiler instances in the same process. We use node:child_process.spawn
474
496
  // which works on Bun, Node.js, and Deno (via compatibility layer).
497
+ // Spawned immediately so it compiles in parallel with the client build above.
475
498
  const workerCmd = resolveWorkerCmd(packageDir);
476
499
  console.log('Spawning SSR worker:', workerCmd.join(' '), 'entry:', entry);
477
500
 
@@ -501,36 +524,41 @@ export const dev = async (options: HadarsRuntimeOptions) => {
501
524
  const marker = 'ssr-watch: initial-build-complete';
502
525
  const rebuildMarker = 'ssr-watch: SSR rebuilt';
503
526
  const decoder = new TextDecoder();
504
- let gotMarker = false;
505
527
  // Hoist so the async continuation loop below can keep using it.
506
528
  let stdoutReader: ReadableStreamDefaultReader<Uint8Array> | null = null;
507
- try {
508
- stdoutReader = stdoutWebStream.getReader();
509
- let buf = '';
510
- const start = Date.now();
511
- const timeoutMs = 20000;
512
- while (Date.now() - start < timeoutMs) {
513
- const { done, value } = await stdoutReader.read();
514
- if (done) { stdoutReader = null; break; }
515
- const chunk = decoder.decode(value, { stream: true });
516
- buf += chunk;
517
- try { process.stdout.write(chunk); } catch (e) { /* ignore */ }
518
- if (buf.includes(marker)) {
519
- gotMarker = true;
520
- break;
529
+ const ssrBuildDone = (async () => {
530
+ let gotMarker = false;
531
+ try {
532
+ stdoutReader = stdoutWebStream.getReader();
533
+ let buf = '';
534
+ const start = Date.now();
535
+ const timeoutMs = 20000;
536
+ while (Date.now() - start < timeoutMs) {
537
+ const { done, value } = await stdoutReader.read();
538
+ if (done) { stdoutReader = null; break; }
539
+ const chunk = decoder.decode(value, { stream: true });
540
+ buf += chunk;
541
+ try { process.stdout.write(chunk); } catch (e) { /* ignore */ }
542
+ if (buf.includes(marker)) {
543
+ gotMarker = true;
544
+ break;
545
+ }
521
546
  }
547
+ if (!gotMarker) {
548
+ console.warn('SSR worker did not signal initial build completion within timeout');
549
+ }
550
+ } catch (err) {
551
+ console.error('Error reading SSR worker output', err);
552
+ stdoutReader = null;
522
553
  }
523
- if (!gotMarker) {
524
- console.warn('SSR worker did not signal initial build completion within timeout');
525
- }
526
- } catch (err) {
527
- console.error('Error reading SSR worker output', err);
528
- stdoutReader = null;
529
- }
554
+ })();
555
+
556
+ // Wait for both client and SSR builds to finish in parallel.
557
+ await Promise.all([clientBuildDone, ssrBuildDone]);
530
558
 
531
559
  // Continue reading stdout to forward logs and pick up SSR rebuild signals.
532
560
  if (stdoutReader) {
533
- const reader = stdoutReader;
561
+ const reader = stdoutReader as ReadableStreamDefaultReader<Uint8Array>;
534
562
  (async () => {
535
563
  try {
536
564
  while (true) {
@@ -577,7 +605,7 @@ export const dev = async (options: HadarsRuntimeOptions) => {
577
605
  const url = new URL(request.url);
578
606
  const path = url.pathname;
579
607
 
580
- // static files in the ninety output folder
608
+ // static files in the hadars output folder
581
609
  const staticRes = await tryServeFile(pathMod.join(__dirname, StaticPath, path));
582
610
  if (staticRes) return staticRes;
583
611
 
@@ -743,7 +771,7 @@ export const run = async (options: HadarsRuntimeOptions) => {
743
771
  const url = new URL(request.url);
744
772
  const path = url.pathname;
745
773
 
746
- // static files in the ninety output folder
774
+ // static files in the hadars output folder
747
775
  const staticRes = await tryServeFile(pathMod.join(__dirname, StaticPath, path));
748
776
  if (staticRes) return staticRes;
749
777
 
package/src/index.tsx CHANGED
@@ -8,7 +8,7 @@ export type {
8
8
  HadarsGetClientProps,
9
9
  HadarsEntryModule,
10
10
  HadarsApp,
11
- } from "./types/ninety";
11
+ } from "./types/hadars";
12
12
  export { Head as HadarsHead, useServerData, initServerDataCache } from './utils/Head';
13
13
  import { AppProviderSSR, AppProviderCSR } from "./utils/Head";
14
14
 
@@ -30,7 +30,7 @@ let _ssrMod: any = null;
30
30
  async function init() {
31
31
  if (_React && _ssrMod) return;
32
32
 
33
- const req = createRequire(pathMod.resolve(process.cwd(), '__ninety_fake__.js'));
33
+ const req = createRequire(pathMod.resolve(process.cwd(), '__hadars_fake__.js'));
34
34
 
35
35
  if (!_React) {
36
36
  const reactPath = pathToFileURL(req.resolve('react')).href;
@@ -63,7 +63,7 @@ export type SerializableRequest = {
63
63
 
64
64
  function deserializeRequest(s: SerializableRequest): any {
65
65
  const init: RequestInit = { method: s.method, headers: new Headers(s.headers) };
66
- if (s.body) init.body = s.body;
66
+ if (s.body) init.body = s.body.buffer as ArrayBuffer;
67
67
  const req = new Request(s.url, init);
68
68
  Object.assign(req, {
69
69
  pathname: s.pathname,
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import type { AppContext, AppUnsuspend, LinkProps, MetaProps, ScriptProps, StyleProps } from '../types/ninety'
2
+ import type { AppContext, AppUnsuspend, LinkProps, MetaProps, ScriptProps, StyleProps } from '../types/hadars'
3
3
 
4
4
  interface InnerContext {
5
5
  setTitle: (title: string) => void;
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { hydrateRoot, createRoot } from 'react-dom/client';
3
- import type { HadarsEntryModule } from '../types/ninety';
3
+ import type { HadarsEntryModule } from '../types/hadars';
4
4
  import { initServerDataCache } from 'hadars';
5
5
  import * as _appMod from '$_MOD_PATH$';
6
6
 
@@ -1,4 +1,4 @@
1
- import type { HadarsOptions, HadarsRequest } from "../types/ninety";
1
+ import type { HadarsOptions, HadarsRequest } from "../types/hadars";
2
2
 
3
3
  type ProxyHandler = (req: HadarsRequest) => ( Promise<Response | undefined> | undefined );
4
4
 
@@ -1,4 +1,4 @@
1
- import type { HadarsRequest } from "../types/ninety";
1
+ import type { HadarsRequest } from "../types/hadars";
2
2
  import { parseCookies } from "./cookies";
3
3
 
4
4
  export const parseRequest = (request: Request): HadarsRequest => {
@@ -2,7 +2,7 @@ import type React from "react";
2
2
  import { createRequire } from "node:module";
3
3
  import pathMod from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
- import type { AppHead, AppUnsuspend, HadarsRequest, HadarsEntryBase, HadarsEntryModule, HadarsProps, AppContext } from "../types/ninety";
5
+ import type { AppHead, AppUnsuspend, HadarsRequest, HadarsEntryBase, HadarsEntryModule, HadarsProps, AppContext } from "../types/hadars";
6
6
 
7
7
  // Resolve react-dom/server from the *project's* node_modules (process.cwd()) so
8
8
  // the same React instance is used here as in the SSR bundle. Without this,
@@ -33,8 +33,8 @@ interface ReactResponseOptions {
33
33
  // ── Head HTML serialisation (no React render needed) ─────────────────────────
34
34
 
35
35
  const ESC: Record<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' };
36
- const escAttr = (s: string) => s.replace(/[&<>"]/g, c => ESC[c]);
37
- const escText = (s: string) => s.replace(/[&<>]/g, c => ESC[c]);
36
+ const escAttr = (s: string) => s.replace(/[&<>"]/g, c => ESC[c] ?? c);
37
+ const escText = (s: string) => s.replace(/[&<>]/g, c => ESC[c] ?? c);
38
38
 
39
39
  // React prop → HTML attribute name for the subset used in head tags.
40
40
  const ATTR: Record<string, string> = {
@@ -2,7 +2,7 @@ import rspack from "@rspack/core";
2
2
  import type { Configuration, RuleSetLoaderWithOptions, RuleSetRule } from "@rspack/core";
3
3
  import ReactRefreshPlugin from '@rspack/plugin-react-refresh';
4
4
  import path from 'node:path';
5
- import type { SwcPluginList } from '../types/ninety';
5
+ import type { SwcPluginList } from '../types/hadars';
6
6
  import { fileURLToPath } from "node:url";
7
7
  import pathMod from "node:path";
8
8
  import { existsSync } from "node:fs";
@@ -41,7 +41,7 @@ export async function tryServeFile(filePath: string): Promise<Response | null> {
41
41
  const data = await readFile(filePath);
42
42
  const ext = filePath.split('.').pop()?.toLowerCase() ?? '';
43
43
  const contentType = MIME[ext] ?? 'application/octet-stream';
44
- return new Response(data, { headers: { 'Content-Type': contentType } });
44
+ return new Response(data.buffer as ArrayBuffer, { headers: { 'Content-Type': contentType } });
45
45
  } catch {
46
46
  return null;
47
47
  }
@@ -3,9 +3,9 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <meta name="NINETY_HEAD">
6
+ <meta name="HADARS_HEAD">
7
7
  </head>
8
8
  <body>
9
- <meta name="NINETY_BODY">
9
+ <meta name="HADARS_BODY">
10
10
  </body>
11
11
  </html>
@@ -1,5 +1,5 @@
1
1
  import type { ServerContext } from './serve';
2
- import type { HadarsOptions, HadarsRequest } from "../types/ninety";
2
+ import type { HadarsOptions, HadarsRequest } from "../types/hadars";
3
3
 
4
4
  type UpgradeHandle = (req: HadarsRequest, ctx: ServerContext) => boolean;
5
5
 
File without changes