hadars 0.1.11 → 0.1.13
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 +1 -1
- package/cli-lib.ts +1 -1
- package/cli.ts +2 -2
- package/dist/cli.js +86 -65
- package/dist/ssr-render-worker.js +2 -2
- package/dist/ssr-watch.js +6 -0
- package/dist/template.html +2 -2
- package/dist/utils/Head.tsx +1 -1
- package/dist/utils/clientScript.tsx +1 -1
- package/index.ts +12 -12
- package/package.json +6 -10
- package/src/build.ts +94 -66
- package/src/index.tsx +1 -1
- package/src/ssr-render-worker.ts +2 -2
- package/src/utils/Head.tsx +1 -1
- package/src/utils/clientScript.tsx +1 -1
- package/src/utils/proxyHandler.tsx +1 -1
- package/src/utils/request.tsx +1 -1
- package/src/utils/response.tsx +3 -3
- package/src/utils/rspack.ts +7 -1
- package/src/utils/staticFile.ts +1 -1
- package/src/utils/template.html +2 -2
- package/src/utils/upgradeRequest.tsx +1 -1
- /package/src/types/{ninety.ts → hadars.ts} +0 -0
package/LICENSE
CHANGED
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/
|
|
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]
|
|
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:
|
|
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 = { "&": "&", "<": "<", ">": ">", '"': """ };
|
|
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",
|
|
@@ -534,6 +534,12 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
|
|
|
534
534
|
experiments: {
|
|
535
535
|
...localConfig.experiments || {},
|
|
536
536
|
outputModule: isServerBuild
|
|
537
|
+
},
|
|
538
|
+
// Prevent rspack from watching its own build output — without this the
|
|
539
|
+
// SSR watcher writing .hadars/index.ssr.js triggers the client compiler
|
|
540
|
+
// and vice versa, causing an infinite rebuild loop.
|
|
541
|
+
watchOptions: {
|
|
542
|
+
ignored: ["**/node_modules/**", "**/.hadars/**"]
|
|
537
543
|
}
|
|
538
544
|
};
|
|
539
545
|
};
|
|
@@ -766,7 +772,7 @@ async function tryServeFile(filePath) {
|
|
|
766
772
|
const data = await readFile(filePath);
|
|
767
773
|
const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
|
|
768
774
|
const contentType = MIME[ext] ?? "application/octet-stream";
|
|
769
|
-
return new Response(data, { headers: { "Content-Type": contentType } });
|
|
775
|
+
return new Response(data.buffer, { headers: { "Content-Type": contentType } });
|
|
770
776
|
} catch {
|
|
771
777
|
return null;
|
|
772
778
|
}
|
|
@@ -784,8 +790,8 @@ import os from "node:os";
|
|
|
784
790
|
import { spawn } from "node:child_process";
|
|
785
791
|
import cluster from "node:cluster";
|
|
786
792
|
var encoder = new TextEncoder();
|
|
787
|
-
var HEAD_MARKER = '<meta name="
|
|
788
|
-
var BODY_MARKER = '<meta name="
|
|
793
|
+
var HEAD_MARKER = '<meta name="HADARS_HEAD">';
|
|
794
|
+
var BODY_MARKER = '<meta name="HADARS_BODY">';
|
|
789
795
|
var _renderToString = null;
|
|
790
796
|
async function getRenderToString() {
|
|
791
797
|
if (!_renderToString) {
|
|
@@ -979,8 +985,30 @@ async function transformStream(data, stream) {
|
|
|
979
985
|
}
|
|
980
986
|
var gzipCompress = (d) => transformStream(d, new globalThis.CompressionStream("gzip"));
|
|
981
987
|
var gzipDecompress = (d) => transformStream(d, new globalThis.DecompressionStream("gzip"));
|
|
988
|
+
async function buildCacheEntry(res, ttl) {
|
|
989
|
+
const buf = await res.arrayBuffer();
|
|
990
|
+
const body = await gzipCompress(new Uint8Array(buf));
|
|
991
|
+
const headers = [];
|
|
992
|
+
res.headers.forEach((v, k) => {
|
|
993
|
+
if (k.toLowerCase() !== "content-encoding" && k.toLowerCase() !== "content-length") {
|
|
994
|
+
headers.push([k, v]);
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
headers.push(["content-encoding", "gzip"]);
|
|
998
|
+
return { body, status: res.status, headers, expiresAt: ttl != null ? Date.now() + ttl : null };
|
|
999
|
+
}
|
|
1000
|
+
async function serveFromEntry(entry, req) {
|
|
1001
|
+
const accept = req.headers.get("Accept-Encoding") ?? "";
|
|
1002
|
+
if (accept.includes("gzip")) {
|
|
1003
|
+
return new Response(entry.body.buffer, { status: entry.status, headers: entry.headers });
|
|
1004
|
+
}
|
|
1005
|
+
const plain = await gzipDecompress(entry.body);
|
|
1006
|
+
const headers = entry.headers.filter(([k]) => k.toLowerCase() !== "content-encoding");
|
|
1007
|
+
return new Response(plain.buffer, { status: entry.status, headers });
|
|
1008
|
+
}
|
|
982
1009
|
function createRenderCache(opts, handler) {
|
|
983
1010
|
const store = /* @__PURE__ */ new Map();
|
|
1011
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
984
1012
|
return async (req, ctx) => {
|
|
985
1013
|
const hadarsReq = parseRequest(req);
|
|
986
1014
|
const cacheOpts = await opts(hadarsReq);
|
|
@@ -988,40 +1016,30 @@ function createRenderCache(opts, handler) {
|
|
|
988
1016
|
if (key != null) {
|
|
989
1017
|
const entry = store.get(key);
|
|
990
1018
|
if (entry) {
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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 });
|
|
1019
|
+
const expired = entry.expiresAt != null && Date.now() >= entry.expiresAt;
|
|
1020
|
+
if (!expired) {
|
|
1021
|
+
return serveFromEntry(entry, req);
|
|
999
1022
|
}
|
|
1000
1023
|
store.delete(key);
|
|
1001
1024
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const headers = [];
|
|
1009
|
-
res.headers.forEach((v, k) => {
|
|
1010
|
-
if (k.toLowerCase() !== "content-encoding" && k.toLowerCase() !== "content-length") {
|
|
1011
|
-
headers.push([k, v]);
|
|
1025
|
+
let flight = inFlight.get(key);
|
|
1026
|
+
if (!flight) {
|
|
1027
|
+
const ttl = cacheOpts?.ttl;
|
|
1028
|
+
flight = handler(new Request(req), ctx).then(async (res) => {
|
|
1029
|
+
if (!res || res.status < 200 || res.status >= 300 || res.headers.has("set-cookie")) {
|
|
1030
|
+
return null;
|
|
1012
1031
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
});
|
|
1032
|
+
const newEntry2 = await buildCacheEntry(res, ttl);
|
|
1033
|
+
store.set(key, newEntry2);
|
|
1034
|
+
return newEntry2;
|
|
1035
|
+
}).catch(() => null).finally(() => inFlight.delete(key));
|
|
1036
|
+
inFlight.set(key, flight);
|
|
1037
|
+
}
|
|
1038
|
+
const newEntry = await flight;
|
|
1039
|
+
if (newEntry)
|
|
1040
|
+
return serveFromEntry(newEntry, req);
|
|
1023
1041
|
}
|
|
1024
|
-
return
|
|
1042
|
+
return handler(req, ctx);
|
|
1025
1043
|
};
|
|
1026
1044
|
}
|
|
1027
1045
|
var SSR_FILENAME = "index.ssr.js";
|
|
@@ -1112,11 +1130,11 @@ var dev = async (options) => {
|
|
|
1112
1130
|
allowedHosts: "all"
|
|
1113
1131
|
}, clientCompiler);
|
|
1114
1132
|
console.log(`Starting HMR dev server on port ${hmrPort}`);
|
|
1115
|
-
|
|
1116
|
-
|
|
1133
|
+
let clientResolved = false;
|
|
1134
|
+
const clientBuildDone = new Promise((resolve2, reject) => {
|
|
1117
1135
|
clientCompiler.hooks.done.tap("initial-build", (stats) => {
|
|
1118
|
-
if (!
|
|
1119
|
-
|
|
1136
|
+
if (!clientResolved) {
|
|
1137
|
+
clientResolved = true;
|
|
1120
1138
|
console.log(stats.toString({ colors: true }));
|
|
1121
1139
|
resolve2();
|
|
1122
1140
|
}
|
|
@@ -1156,37 +1174,40 @@ var dev = async (options) => {
|
|
|
1156
1174
|
const marker = "ssr-watch: initial-build-complete";
|
|
1157
1175
|
const rebuildMarker = "ssr-watch: SSR rebuilt";
|
|
1158
1176
|
const decoder = new TextDecoder();
|
|
1159
|
-
let gotMarker = false;
|
|
1160
1177
|
let stdoutReader = null;
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
const
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1178
|
+
const ssrBuildDone = (async () => {
|
|
1179
|
+
let gotMarker = false;
|
|
1180
|
+
try {
|
|
1181
|
+
stdoutReader = stdoutWebStream.getReader();
|
|
1182
|
+
let buf = "";
|
|
1183
|
+
const start = Date.now();
|
|
1184
|
+
const timeoutMs = 2e4;
|
|
1185
|
+
while (Date.now() - start < timeoutMs) {
|
|
1186
|
+
const { done, value } = await stdoutReader.read();
|
|
1187
|
+
if (done) {
|
|
1188
|
+
stdoutReader = null;
|
|
1189
|
+
break;
|
|
1190
|
+
}
|
|
1191
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
1192
|
+
buf += chunk;
|
|
1193
|
+
try {
|
|
1194
|
+
process.stdout.write(chunk);
|
|
1195
|
+
} catch (e) {
|
|
1196
|
+
}
|
|
1197
|
+
if (buf.includes(marker)) {
|
|
1198
|
+
gotMarker = true;
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1177
1201
|
}
|
|
1178
|
-
if (
|
|
1179
|
-
|
|
1180
|
-
break;
|
|
1202
|
+
if (!gotMarker) {
|
|
1203
|
+
console.warn("SSR worker did not signal initial build completion within timeout");
|
|
1181
1204
|
}
|
|
1205
|
+
} catch (err) {
|
|
1206
|
+
console.error("Error reading SSR worker output", err);
|
|
1207
|
+
stdoutReader = null;
|
|
1182
1208
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
}
|
|
1186
|
-
} catch (err) {
|
|
1187
|
-
console.error("Error reading SSR worker output", err);
|
|
1188
|
-
stdoutReader = null;
|
|
1189
|
-
}
|
|
1209
|
+
})();
|
|
1210
|
+
await Promise.all([clientBuildDone, ssrBuildDone]);
|
|
1190
1211
|
if (stdoutReader) {
|
|
1191
1212
|
const reader = stdoutReader;
|
|
1192
1213
|
(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(), "
|
|
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,
|
package/dist/ssr-watch.js
CHANGED
|
@@ -260,6 +260,12 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
|
|
|
260
260
|
experiments: {
|
|
261
261
|
...localConfig.experiments || {},
|
|
262
262
|
outputModule: isServerBuild
|
|
263
|
+
},
|
|
264
|
+
// Prevent rspack from watching its own build output — without this the
|
|
265
|
+
// SSR watcher writing .hadars/index.ssr.js triggers the client compiler
|
|
266
|
+
// and vice versa, causing an infinite rebuild loop.
|
|
267
|
+
watchOptions: {
|
|
268
|
+
ignored: ["**/node_modules/**", "**/.hadars/**"]
|
|
263
269
|
}
|
|
264
270
|
};
|
|
265
271
|
};
|
package/dist/template.html
CHANGED
package/dist/utils/Head.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { AppContext, AppUnsuspend, LinkProps, MetaProps, ScriptProps, StyleProps } from '../types/
|
|
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/
|
|
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
|
-
//
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from "./src/types/
|
|
15
|
-
export {
|
|
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.
|
|
3
|
+
"version": "0.1.13",
|
|
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
|
-
"
|
|
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.
|
|
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/
|
|
19
|
+
import type { HadarsEntryModule, HadarsOptions, HadarsProps } from "./types/hadars";
|
|
20
20
|
const encoder = new TextEncoder();
|
|
21
21
|
|
|
22
|
-
const HEAD_MARKER = '<meta name="
|
|
23
|
-
const BODY_MARKER = '<meta name="
|
|
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
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
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
|
-
|
|
461
|
-
|
|
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 (!
|
|
464
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
|
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
|
|
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/
|
|
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
|
|
package/src/ssr-render-worker.ts
CHANGED
|
@@ -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(), '
|
|
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,
|
package/src/utils/Head.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { AppContext, AppUnsuspend, LinkProps, MetaProps, ScriptProps, StyleProps } from '../types/
|
|
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/
|
|
3
|
+
import type { HadarsEntryModule } from '../types/hadars';
|
|
4
4
|
import { initServerDataCache } from 'hadars';
|
|
5
5
|
import * as _appMod from '$_MOD_PATH$';
|
|
6
6
|
|
package/src/utils/request.tsx
CHANGED
package/src/utils/response.tsx
CHANGED
|
@@ -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/
|
|
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> = { '&': '&', '<': '<', '>': '>', '"': '"' };
|
|
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> = {
|
package/src/utils/rspack.ts
CHANGED
|
@@ -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/
|
|
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";
|
|
@@ -314,6 +314,12 @@ const buildCompilerConfig = (
|
|
|
314
314
|
...(localConfig.experiments || {}),
|
|
315
315
|
outputModule: isServerBuild,
|
|
316
316
|
},
|
|
317
|
+
// Prevent rspack from watching its own build output — without this the
|
|
318
|
+
// SSR watcher writing .hadars/index.ssr.js triggers the client compiler
|
|
319
|
+
// and vice versa, causing an infinite rebuild loop.
|
|
320
|
+
watchOptions: {
|
|
321
|
+
ignored: ['**/node_modules/**', '**/.hadars/**'],
|
|
322
|
+
},
|
|
317
323
|
};
|
|
318
324
|
};
|
|
319
325
|
|
package/src/utils/staticFile.ts
CHANGED
|
@@ -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
|
}
|
package/src/utils/template.html
CHANGED
|
File without changes
|