hadars 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +75 -2
- package/dist/index.d.ts +23 -0
- package/package.json +15 -15
- package/src/build.ts +98 -2
- package/src/types/ninety.ts +18 -0
package/dist/cli.js
CHANGED
|
@@ -956,6 +956,74 @@ var makePrecontentHtmlGetter = (htmlFilePromise) => {
|
|
|
956
956
|
return [preHead + headHtml + postHead, postContent];
|
|
957
957
|
};
|
|
958
958
|
};
|
|
959
|
+
async function transformStream(data, stream) {
|
|
960
|
+
const writer = stream.writable.getWriter();
|
|
961
|
+
writer.write(data);
|
|
962
|
+
writer.close();
|
|
963
|
+
const chunks = [];
|
|
964
|
+
const reader = stream.readable.getReader();
|
|
965
|
+
while (true) {
|
|
966
|
+
const { done, value } = await reader.read();
|
|
967
|
+
if (done)
|
|
968
|
+
break;
|
|
969
|
+
chunks.push(value);
|
|
970
|
+
}
|
|
971
|
+
const total = chunks.reduce((n, c) => n + c.length, 0);
|
|
972
|
+
const out = new Uint8Array(total);
|
|
973
|
+
let offset = 0;
|
|
974
|
+
for (const c of chunks) {
|
|
975
|
+
out.set(c, offset);
|
|
976
|
+
offset += c.length;
|
|
977
|
+
}
|
|
978
|
+
return out;
|
|
979
|
+
}
|
|
980
|
+
var gzipCompress = (d) => transformStream(d, new globalThis.CompressionStream("gzip"));
|
|
981
|
+
var gzipDecompress = (d) => transformStream(d, new globalThis.DecompressionStream("gzip"));
|
|
982
|
+
function createRenderCache(opts, handler) {
|
|
983
|
+
const store = /* @__PURE__ */ new Map();
|
|
984
|
+
return async (req, ctx) => {
|
|
985
|
+
const hadarsReq = parseRequest(req);
|
|
986
|
+
const cacheOpts = await opts(hadarsReq);
|
|
987
|
+
const key = cacheOpts?.key ?? null;
|
|
988
|
+
if (key != null) {
|
|
989
|
+
const entry = store.get(key);
|
|
990
|
+
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 });
|
|
999
|
+
}
|
|
1000
|
+
store.delete(key);
|
|
1001
|
+
}
|
|
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]);
|
|
1012
|
+
}
|
|
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
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
return res;
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
959
1027
|
var SSR_FILENAME = "index.ssr.js";
|
|
960
1028
|
var __dirname2 = process.cwd();
|
|
961
1029
|
var getSuffix = (mode) => mode === "development" ? `?v=${Date.now()}` : "";
|
|
@@ -1295,7 +1363,7 @@ var run = async (options) => {
|
|
|
1295
1363
|
const getPrecontentHtml = makePrecontentHtmlGetter(
|
|
1296
1364
|
fs.readFile(pathMod3.join(__dirname2, StaticPath, "out.html"), "utf-8")
|
|
1297
1365
|
);
|
|
1298
|
-
|
|
1366
|
+
const runHandler = async (req, ctx) => {
|
|
1299
1367
|
const request = parseRequest(req);
|
|
1300
1368
|
if (handler) {
|
|
1301
1369
|
const res = await handler(request);
|
|
@@ -1363,7 +1431,12 @@ var run = async (options) => {
|
|
|
1363
1431
|
console.error("[hadars] SSR render error:", err);
|
|
1364
1432
|
return new Response("Internal Server Error", { status: 500 });
|
|
1365
1433
|
}
|
|
1366
|
-
}
|
|
1434
|
+
};
|
|
1435
|
+
await serve(
|
|
1436
|
+
port,
|
|
1437
|
+
options.cache ? createRenderCache(options.cache, runHandler) : runHandler,
|
|
1438
|
+
options.websocket
|
|
1439
|
+
);
|
|
1367
1440
|
};
|
|
1368
1441
|
|
|
1369
1442
|
// cli-lib.ts
|
package/dist/index.d.ts
CHANGED
|
@@ -92,6 +92,29 @@ interface HadarsOptions {
|
|
|
92
92
|
* Has no effect on the SSR bundle or dev mode.
|
|
93
93
|
*/
|
|
94
94
|
optimization?: Record<string, unknown>;
|
|
95
|
+
/**
|
|
96
|
+
* SSR response cache for `run()` mode. Has no effect in `dev()` mode.
|
|
97
|
+
*
|
|
98
|
+
* Receives the incoming request and should return `{ key, ttl? }` to cache
|
|
99
|
+
* the response, or `null`/`undefined` to skip caching for that request.
|
|
100
|
+
* `ttl` is the time-to-live in milliseconds; omit for entries that never expire.
|
|
101
|
+
* The function may be async.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* // Cache every page by pathname (no per-user personalisation):
|
|
105
|
+
* cache: (req) => ({ key: req.pathname })
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* // Cache with a per-route TTL, skip authenticated requests:
|
|
109
|
+
* cache: (req) => req.cookies.session ? null : { key: req.pathname, ttl: 60_000 }
|
|
110
|
+
*/
|
|
111
|
+
cache?: (req: HadarsRequest) => {
|
|
112
|
+
key: string;
|
|
113
|
+
ttl?: number;
|
|
114
|
+
} | null | undefined | Promise<{
|
|
115
|
+
key: string;
|
|
116
|
+
ttl?: number;
|
|
117
|
+
} | null | undefined>;
|
|
95
118
|
}
|
|
96
119
|
type SwcPluginItem = string | [string, Record<string, unknown>] | {
|
|
97
120
|
path: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hadars",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Minimal SSR framework for React — rspack, HMR, TypeScript, Bun/Node/Deno",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"node": ">=18.0.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"react": "^19.
|
|
56
|
-
"react-dom": "^19.
|
|
55
|
+
"react": "^19.1.1",
|
|
56
|
+
"react-dom": "^19.1.1",
|
|
57
57
|
"typescript": "^5"
|
|
58
58
|
},
|
|
59
59
|
"peerDependenciesMeta": {
|
|
@@ -63,28 +63,28 @@
|
|
|
63
63
|
},
|
|
64
64
|
"optionalDependencies": {
|
|
65
65
|
"@rspack/core": "1.4.9",
|
|
66
|
-
"@rspack/dev-server": "^1.1
|
|
67
|
-
"@rspack/plugin-react-refresh": "^1.
|
|
68
|
-
"@swc/core": "^1.
|
|
66
|
+
"@rspack/dev-server": "^1.2.1",
|
|
67
|
+
"@rspack/plugin-react-refresh": "^1.6.1",
|
|
68
|
+
"@swc/core": "^1.15.18",
|
|
69
69
|
"@types/bun": "latest",
|
|
70
|
-
"@types/react-dom": "^19.
|
|
70
|
+
"@types/react-dom": "^19.2.3",
|
|
71
71
|
"react-refresh": "^0.17.0",
|
|
72
|
-
"typescript": "^5"
|
|
72
|
+
"typescript": "^5.9.3"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@types/react": "^19.
|
|
75
|
+
"@types/react": "^19.2.14",
|
|
76
76
|
"@types/react-dom": "^19.0.0",
|
|
77
|
-
"esbuild": "^0.19.
|
|
78
|
-
"tsup": "^6.
|
|
77
|
+
"esbuild": "^0.19.12",
|
|
78
|
+
"tsup": "^6.7.0"
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@mdx-js/loader": "^3.1.1",
|
|
82
82
|
"@mdx-js/react": "^3.1.1",
|
|
83
83
|
"@svgr/webpack": "^8.1.0",
|
|
84
|
-
"@tailwindcss/postcss": "^4.1
|
|
85
|
-
"postcss": "^8.5.
|
|
86
|
-
"postcss-loader": "^8.2.
|
|
87
|
-
"tailwindcss": "^4.1
|
|
84
|
+
"@tailwindcss/postcss": "^4.2.1",
|
|
85
|
+
"postcss": "^8.5.8",
|
|
86
|
+
"postcss-loader": "^8.2.1",
|
|
87
|
+
"tailwindcss": "^4.2.1"
|
|
88
88
|
},
|
|
89
89
|
"license": "MIT",
|
|
90
90
|
"repository": {
|
package/src/build.ts
CHANGED
|
@@ -230,6 +230,96 @@ const makePrecontentHtmlGetter = (htmlFilePromise: Promise<string>) => {
|
|
|
230
230
|
};
|
|
231
231
|
};
|
|
232
232
|
|
|
233
|
+
// ── SSR response cache ────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
interface CacheEntry {
|
|
236
|
+
/** Gzip-compressed response body — much cheaper to keep in RAM than raw HTML. */
|
|
237
|
+
body: Uint8Array;
|
|
238
|
+
status: number;
|
|
239
|
+
/** Headers with Content-Encoding: gzip already set. */
|
|
240
|
+
headers: [string, string][];
|
|
241
|
+
expiresAt: number | null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
type CacheFetchHandler = (req: Request, ctx: any) => Promise<Response | undefined>;
|
|
245
|
+
|
|
246
|
+
async function transformStream(data: Uint8Array, stream: { writable: WritableStream; readable: ReadableStream<Uint8Array> }): Promise<Uint8Array> {
|
|
247
|
+
const writer = stream.writable.getWriter();
|
|
248
|
+
writer.write(data);
|
|
249
|
+
writer.close();
|
|
250
|
+
const chunks: Uint8Array[] = [];
|
|
251
|
+
const reader = stream.readable.getReader();
|
|
252
|
+
while (true) {
|
|
253
|
+
const { done, value } = await reader.read();
|
|
254
|
+
if (done) break;
|
|
255
|
+
chunks.push(value);
|
|
256
|
+
}
|
|
257
|
+
const total = chunks.reduce((n, c) => n + c.length, 0);
|
|
258
|
+
const out = new Uint8Array(total);
|
|
259
|
+
let offset = 0;
|
|
260
|
+
for (const c of chunks) { out.set(c, offset); offset += c.length; }
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const gzipCompress = (d: Uint8Array) => transformStream(d, new (globalThis as any).CompressionStream('gzip'));
|
|
265
|
+
const gzipDecompress = (d: Uint8Array) => transformStream(d, new (globalThis as any).DecompressionStream('gzip'));
|
|
266
|
+
|
|
267
|
+
function createRenderCache(
|
|
268
|
+
opts: NonNullable<HadarsOptions['cache']>,
|
|
269
|
+
handler: CacheFetchHandler,
|
|
270
|
+
): CacheFetchHandler {
|
|
271
|
+
const store = new Map<string, CacheEntry>();
|
|
272
|
+
|
|
273
|
+
return async (req, ctx) => {
|
|
274
|
+
const hadarsReq = parseRequest(req);
|
|
275
|
+
const cacheOpts = await opts(hadarsReq);
|
|
276
|
+
const key = cacheOpts?.key ?? null;
|
|
277
|
+
|
|
278
|
+
if (key != null) {
|
|
279
|
+
const entry = store.get(key);
|
|
280
|
+
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 });
|
|
290
|
+
}
|
|
291
|
+
store.delete(key);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
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 */ });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return res;
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
233
323
|
interface HadarsRuntimeOptions extends HadarsOptions {
|
|
234
324
|
mode: "development" | "production";
|
|
235
325
|
}
|
|
@@ -639,7 +729,7 @@ export const run = async (options: HadarsRuntimeOptions) => {
|
|
|
639
729
|
fs.readFile(pathMod.join(__dirname, StaticPath, 'out.html'), 'utf-8')
|
|
640
730
|
);
|
|
641
731
|
|
|
642
|
-
|
|
732
|
+
const runHandler: CacheFetchHandler = async (req, ctx) => {
|
|
643
733
|
const request = parseRequest(req);
|
|
644
734
|
if (handler) {
|
|
645
735
|
const res = await handler(request);
|
|
@@ -715,5 +805,11 @@ export const run = async (options: HadarsRuntimeOptions) => {
|
|
|
715
805
|
console.error('[hadars] SSR render error:', err);
|
|
716
806
|
return new Response('Internal Server Error', { status: 500 });
|
|
717
807
|
}
|
|
718
|
-
}
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
await serve(
|
|
811
|
+
port,
|
|
812
|
+
options.cache ? createRenderCache(options.cache, runHandler) : runHandler,
|
|
813
|
+
options.websocket,
|
|
814
|
+
);
|
|
719
815
|
};
|
package/src/types/ninety.ts
CHANGED
|
@@ -93,6 +93,24 @@ export interface HadarsOptions {
|
|
|
93
93
|
* Has no effect on the SSR bundle or dev mode.
|
|
94
94
|
*/
|
|
95
95
|
optimization?: Record<string, unknown>;
|
|
96
|
+
/**
|
|
97
|
+
* SSR response cache for `run()` mode. Has no effect in `dev()` mode.
|
|
98
|
+
*
|
|
99
|
+
* Receives the incoming request and should return `{ key, ttl? }` to cache
|
|
100
|
+
* the response, or `null`/`undefined` to skip caching for that request.
|
|
101
|
+
* `ttl` is the time-to-live in milliseconds; omit for entries that never expire.
|
|
102
|
+
* The function may be async.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* // Cache every page by pathname (no per-user personalisation):
|
|
106
|
+
* cache: (req) => ({ key: req.pathname })
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* // Cache with a per-route TTL, skip authenticated requests:
|
|
110
|
+
* cache: (req) => req.cookies.session ? null : { key: req.pathname, ttl: 60_000 }
|
|
111
|
+
*/
|
|
112
|
+
cache?: (req: HadarsRequest) => { key: string; ttl?: number } | null | undefined
|
|
113
|
+
| Promise<{ key: string; ttl?: number } | null | undefined>;
|
|
96
114
|
}
|
|
97
115
|
|
|
98
116
|
|