hadars 0.4.1 → 0.4.2
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/{chunk-TV37IMRB.js → chunk-2TMQUXFL.js} +10 -10
- package/dist/{chunk-2J2L2H3H.js → chunk-NYLXE7T7.js} +6 -6
- package/dist/{chunk-OS3V4CPN.js → chunk-OZUZS2PD.js} +4 -4
- package/dist/cli.js +462 -496
- package/dist/cloudflare.cjs +11 -11
- package/dist/cloudflare.js +3 -3
- package/dist/index.d.cts +8 -4
- package/dist/index.d.ts +8 -4
- package/dist/lambda.cjs +11 -11
- package/dist/lambda.js +7 -7
- package/dist/loader.cjs +90 -54
- package/dist/slim-react/index.cjs +13 -13
- package/dist/slim-react/index.js +2 -2
- package/dist/slim-react/jsx-runtime.cjs +2 -4
- package/dist/slim-react/jsx-runtime.js +1 -1
- package/dist/ssr-render-worker.js +174 -161
- package/dist/ssr-watch.js +40 -74
- package/package.json +8 -10
- package/cli-lib.ts +0 -676
- package/cli.ts +0 -36
- package/index.ts +0 -17
- package/src/build.ts +0 -805
- package/src/cloudflare.ts +0 -140
- package/src/index.tsx +0 -41
- package/src/lambda.ts +0 -287
- package/src/slim-react/context.ts +0 -55
- package/src/slim-react/dispatcher.ts +0 -87
- package/src/slim-react/hooks.ts +0 -137
- package/src/slim-react/index.ts +0 -232
- package/src/slim-react/jsx-runtime.ts +0 -7
- package/src/slim-react/jsx.ts +0 -53
- package/src/slim-react/render.ts +0 -1101
- package/src/slim-react/renderContext.ts +0 -294
- package/src/slim-react/types.ts +0 -33
- package/src/source/context.ts +0 -113
- package/src/source/graphiql.ts +0 -101
- package/src/source/inference.ts +0 -260
- package/src/source/runner.ts +0 -138
- package/src/source/store.ts +0 -50
- package/src/ssr-render-worker.ts +0 -116
- package/src/ssr-watch.ts +0 -62
- package/src/static.ts +0 -109
- package/src/types/global.d.ts +0 -5
- package/src/types/hadars.ts +0 -350
- package/src/utils/Head.tsx +0 -462
- package/src/utils/clientScript.tsx +0 -71
- package/src/utils/cookies.ts +0 -16
- package/src/utils/loader.ts +0 -335
- package/src/utils/proxyHandler.tsx +0 -104
- package/src/utils/request.tsx +0 -9
- package/src/utils/response.tsx +0 -141
- package/src/utils/rspack.ts +0 -467
- package/src/utils/runtime.ts +0 -19
- package/src/utils/serve.ts +0 -155
- package/src/utils/ssrHandler.ts +0 -239
- package/src/utils/staticFile.ts +0 -43
- package/src/utils/template.html +0 -11
- package/src/utils/upgradeRequest.tsx +0 -19
package/src/utils/ssrHandler.ts
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { parseRequest } from './request';
|
|
2
|
-
import { buildHeadHtml } from './response';
|
|
3
|
-
import type { AppHead, HadarsOptions } from '../types/hadars';
|
|
4
|
-
|
|
5
|
-
export const HEAD_MARKER = '<meta name="HADARS_HEAD">';
|
|
6
|
-
export const BODY_MARKER = '<meta name="HADARS_BODY">';
|
|
7
|
-
|
|
8
|
-
const encoder = new TextEncoder();
|
|
9
|
-
|
|
10
|
-
// ── HTML response assembly ────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
export function buildSsrResponse(
|
|
13
|
-
head: AppHead,
|
|
14
|
-
status: number,
|
|
15
|
-
getAppBody: () => Promise<string>,
|
|
16
|
-
finalize: () => Promise<{ clientProps: Record<string, unknown> }>,
|
|
17
|
-
getPrecontentHtml: (headHtml: string) => [string, string] | Promise<[string, string]>,
|
|
18
|
-
): Response {
|
|
19
|
-
const headHtml = buildHeadHtml(head);
|
|
20
|
-
const precontentResult = getPrecontentHtml(headHtml);
|
|
21
|
-
|
|
22
|
-
const responseStream = new ReadableStream({
|
|
23
|
-
async start(controller) {
|
|
24
|
-
try {
|
|
25
|
-
// Resolve the template — sync on the hot path (every request after the first).
|
|
26
|
-
const [precontentHtml, postContent] = precontentResult instanceof Promise
|
|
27
|
-
? await precontentResult
|
|
28
|
-
: precontentResult;
|
|
29
|
-
|
|
30
|
-
// Chunk 1 — flush the full <head> shell immediately so the browser
|
|
31
|
-
// can start loading CSS / fonts / preload hints before the body arrives.
|
|
32
|
-
controller.enqueue(encoder.encode(precontentHtml));
|
|
33
|
-
|
|
34
|
-
// Chunk 2 — body HTML. getAppBody() triggers the actual renderToString
|
|
35
|
-
// now that head has been flushed. All data is cached from the preflight
|
|
36
|
-
// so this pass is fast (no async waits).
|
|
37
|
-
const bodyHtml = await getAppBody();
|
|
38
|
-
controller.enqueue(encoder.encode(`<div id="app">${bodyHtml}</div>`));
|
|
39
|
-
|
|
40
|
-
// Chunk 3 — JSON props script + post-content. Separated so the browser
|
|
41
|
-
// can parse/render the body while getFinalProps is still completing.
|
|
42
|
-
const { clientProps } = await finalize();
|
|
43
|
-
const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, '\\u003c');
|
|
44
|
-
controller.enqueue(encoder.encode(
|
|
45
|
-
`<script id="hadars" type="application/json">${scriptContent}</script>` +
|
|
46
|
-
postContent,
|
|
47
|
-
));
|
|
48
|
-
controller.close();
|
|
49
|
-
} catch (err) {
|
|
50
|
-
// Head chunk may already be sent; signal a stream error so the
|
|
51
|
-
// connection is closed cleanly rather than hanging.
|
|
52
|
-
controller.error(err);
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
return new Response(responseStream, {
|
|
57
|
-
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
58
|
-
status,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Like {@link buildSsrResponse} but returns the complete HTML string directly
|
|
64
|
-
* instead of wrapping it in a streaming Response. Use this in environments
|
|
65
|
-
* where streaming is not beneficial (e.g. AWS Lambda) to avoid the
|
|
66
|
-
* ReadableStream allocation and the subsequent `.text()` drain overhead.
|
|
67
|
-
*/
|
|
68
|
-
export async function buildSsrHtml(
|
|
69
|
-
bodyHtml: string,
|
|
70
|
-
clientProps: Record<string, unknown>,
|
|
71
|
-
headHtml: string,
|
|
72
|
-
getPrecontentHtml: (headHtml: string) => [string, string] | Promise<[string, string]>,
|
|
73
|
-
): Promise<string> {
|
|
74
|
-
const [precontentHtml, postContent] = await Promise.resolve(getPrecontentHtml(headHtml));
|
|
75
|
-
const scriptContent = JSON.stringify({ hadars: { props: clientProps } }).replace(/</g, '\\u003c');
|
|
76
|
-
return (
|
|
77
|
-
precontentHtml +
|
|
78
|
-
`<div id="app">${bodyHtml}</div><script id="hadars" type="application/json">${scriptContent}</script>` +
|
|
79
|
-
postContent
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Returns a function that parses `out.html` into pre-head, post-head, and
|
|
85
|
-
* post-content segments and caches the result. Call the returned function with
|
|
86
|
-
* the per-request headHtml to produce the full HTML prefix and suffix.
|
|
87
|
-
*/
|
|
88
|
-
export const makePrecontentHtmlGetter = (htmlFilePromise: Promise<string>) => {
|
|
89
|
-
let preHead: string | null = null;
|
|
90
|
-
let postHead: string | null = null;
|
|
91
|
-
let postContent: string | null = null;
|
|
92
|
-
|
|
93
|
-
// Parse the template as soon as the file read completes — during the
|
|
94
|
-
// process's idle time before the first request arrives. This way the
|
|
95
|
-
// first real request finds preHead !== null and takes the sync path.
|
|
96
|
-
const primed = htmlFilePromise.then(html => {
|
|
97
|
-
const headEnd = html.indexOf(HEAD_MARKER);
|
|
98
|
-
const contentStart = html.indexOf(BODY_MARKER);
|
|
99
|
-
preHead = html.slice(0, headEnd);
|
|
100
|
-
postHead = html.slice(headEnd + HEAD_MARKER.length, contentStart);
|
|
101
|
-
postContent = html.slice(contentStart + BODY_MARKER.length);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// Returns synchronously once the template has been loaded and parsed
|
|
105
|
-
// (every request after the first). Callers can check `instanceof Promise`
|
|
106
|
-
// to take a zero-await hot path.
|
|
107
|
-
return (headHtml: string): [string, string] | Promise<[string, string]> => {
|
|
108
|
-
if (preHead !== null) {
|
|
109
|
-
// Hot path — sync return, no Promise allocation.
|
|
110
|
-
return [preHead + headHtml + postHead!, postContent!];
|
|
111
|
-
}
|
|
112
|
-
return primed.then(() => [preHead! + headHtml + postHead!, postContent!]);
|
|
113
|
-
};
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
// ── SSR response cache ────────────────────────────────────────────────────────
|
|
117
|
-
|
|
118
|
-
export interface CacheEntry {
|
|
119
|
-
/** Gzip-compressed response body. */
|
|
120
|
-
body: Uint8Array;
|
|
121
|
-
status: number;
|
|
122
|
-
/** Headers with Content-Encoding: gzip already set. */
|
|
123
|
-
headers: [string, string][];
|
|
124
|
-
expiresAt: number | null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export type CacheFetchHandler = (req: Request, ctx: any) => Promise<Response | undefined>;
|
|
128
|
-
|
|
129
|
-
async function transformStream(
|
|
130
|
-
data: Uint8Array,
|
|
131
|
-
stream: { writable: WritableStream; readable: ReadableStream<Uint8Array> },
|
|
132
|
-
): Promise<Uint8Array> {
|
|
133
|
-
const writer = stream.writable.getWriter();
|
|
134
|
-
writer.write(data);
|
|
135
|
-
writer.close();
|
|
136
|
-
const chunks: Uint8Array[] = [];
|
|
137
|
-
const reader = stream.readable.getReader();
|
|
138
|
-
while (true) {
|
|
139
|
-
const { done, value } = await reader.read();
|
|
140
|
-
if (done) break;
|
|
141
|
-
chunks.push(value);
|
|
142
|
-
}
|
|
143
|
-
const total = chunks.reduce((n, c) => n + c.length, 0);
|
|
144
|
-
const out = new Uint8Array(total);
|
|
145
|
-
let offset = 0;
|
|
146
|
-
for (const c of chunks) { out.set(c, offset); offset += c.length; }
|
|
147
|
-
return out;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Use the Web Streams CompressionStream when available (Bun, Deno, Node ≥ 18).
|
|
151
|
-
// Fall back to node:zlib on older Node versions.
|
|
152
|
-
async function zlibGzip(d: Uint8Array): Promise<Uint8Array> {
|
|
153
|
-
const zlib = await import('node:zlib');
|
|
154
|
-
const { promisify } = await import('node:util');
|
|
155
|
-
return promisify(zlib.gzip)(d) as Promise<Uint8Array>;
|
|
156
|
-
}
|
|
157
|
-
async function zlibGunzip(d: Uint8Array): Promise<Uint8Array> {
|
|
158
|
-
const zlib = await import('node:zlib');
|
|
159
|
-
const { promisify } = await import('node:util');
|
|
160
|
-
return promisify(zlib.gunzip)(d) as Promise<Uint8Array>;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export const gzipCompress = (d: Uint8Array): Promise<Uint8Array> =>
|
|
164
|
-
(globalThis as any).CompressionStream
|
|
165
|
-
? transformStream(d, new (globalThis as any).CompressionStream('gzip'))
|
|
166
|
-
: zlibGzip(d);
|
|
167
|
-
|
|
168
|
-
export const gzipDecompress = (d: Uint8Array): Promise<Uint8Array> =>
|
|
169
|
-
(globalThis as any).DecompressionStream
|
|
170
|
-
? transformStream(d, new (globalThis as any).DecompressionStream('gzip'))
|
|
171
|
-
: zlibGunzip(d);
|
|
172
|
-
|
|
173
|
-
export async function buildCacheEntry(res: Response, ttl: number | undefined): Promise<CacheEntry> {
|
|
174
|
-
const buf = await res.arrayBuffer();
|
|
175
|
-
const body = await gzipCompress(new Uint8Array(buf));
|
|
176
|
-
const headers: [string, string][] = [];
|
|
177
|
-
res.headers.forEach((v, k) => {
|
|
178
|
-
if (k.toLowerCase() !== 'content-encoding' && k.toLowerCase() !== 'content-length') {
|
|
179
|
-
headers.push([k, v]);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
headers.push(['content-encoding', 'gzip']);
|
|
183
|
-
return { body, status: res.status, headers, expiresAt: ttl != null ? Date.now() + ttl : null };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export async function serveFromEntry(entry: CacheEntry, req: Request): Promise<Response> {
|
|
187
|
-
const accept = req.headers.get('Accept-Encoding') ?? '';
|
|
188
|
-
if (accept.includes('gzip')) {
|
|
189
|
-
return new Response(entry.body.buffer as ArrayBuffer, { status: entry.status, headers: entry.headers });
|
|
190
|
-
}
|
|
191
|
-
const plain = await gzipDecompress(entry.body);
|
|
192
|
-
const headers = entry.headers.filter(([k]) => k.toLowerCase() !== 'content-encoding');
|
|
193
|
-
return new Response(plain.buffer as ArrayBuffer, { status: entry.status, headers });
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export function createRenderCache(
|
|
197
|
-
opts: NonNullable<HadarsOptions['cache']>,
|
|
198
|
-
handler: CacheFetchHandler,
|
|
199
|
-
): CacheFetchHandler {
|
|
200
|
-
const store = new Map<string, CacheEntry>();
|
|
201
|
-
const inFlight = new Map<string, Promise<CacheEntry | null>>();
|
|
202
|
-
|
|
203
|
-
return async (req, ctx) => {
|
|
204
|
-
const hadarsReq = parseRequest(req);
|
|
205
|
-
const cacheOpts = await opts(hadarsReq);
|
|
206
|
-
const key = cacheOpts?.key ?? null;
|
|
207
|
-
|
|
208
|
-
if (key != null) {
|
|
209
|
-
const entry = store.get(key);
|
|
210
|
-
if (entry) {
|
|
211
|
-
const expired = entry.expiresAt != null && Date.now() >= entry.expiresAt;
|
|
212
|
-
if (!expired) return serveFromEntry(entry, req);
|
|
213
|
-
store.delete(key);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
let flight = inFlight.get(key);
|
|
217
|
-
if (!flight) {
|
|
218
|
-
const ttl = cacheOpts?.ttl;
|
|
219
|
-
flight = handler(new Request(req), ctx)
|
|
220
|
-
.then(async (res) => {
|
|
221
|
-
if (!res || res.status < 200 || res.status >= 300 || res.headers.has('set-cookie')) {
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
const newEntry = await buildCacheEntry(res, ttl);
|
|
225
|
-
store.set(key, newEntry);
|
|
226
|
-
return newEntry;
|
|
227
|
-
})
|
|
228
|
-
.catch(() => null)
|
|
229
|
-
.finally(() => inFlight.delete(key));
|
|
230
|
-
inFlight.set(key, flight);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const newEntry = await flight;
|
|
234
|
-
if (newEntry) return serveFromEntry(newEntry, req);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return handler(req, ctx);
|
|
238
|
-
};
|
|
239
|
-
}
|
package/src/utils/staticFile.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
|
|
3
|
-
/** MIME type map keyed by lowercase file extension. */
|
|
4
|
-
const MIME: Record<string, string> = {
|
|
5
|
-
html: 'text/html; charset=utf-8',
|
|
6
|
-
htm: 'text/html; charset=utf-8',
|
|
7
|
-
css: 'text/css',
|
|
8
|
-
js: 'application/javascript',
|
|
9
|
-
mjs: 'application/javascript',
|
|
10
|
-
cjs: 'application/javascript',
|
|
11
|
-
json: 'application/json',
|
|
12
|
-
map: 'application/json',
|
|
13
|
-
png: 'image/png',
|
|
14
|
-
jpg: 'image/jpeg',
|
|
15
|
-
jpeg: 'image/jpeg',
|
|
16
|
-
gif: 'image/gif',
|
|
17
|
-
webp: 'image/webp',
|
|
18
|
-
svg: 'image/svg+xml',
|
|
19
|
-
ico: 'image/x-icon',
|
|
20
|
-
woff: 'font/woff',
|
|
21
|
-
woff2: 'font/woff2',
|
|
22
|
-
ttf: 'font/ttf',
|
|
23
|
-
otf: 'font/otf',
|
|
24
|
-
txt: 'text/plain',
|
|
25
|
-
xml: 'application/xml',
|
|
26
|
-
pdf: 'application/pdf',
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Tries to serve a file at the given absolute path.
|
|
31
|
-
* Returns a Response with the correct Content-Type, or null if the file does
|
|
32
|
-
* not exist or cannot be read.
|
|
33
|
-
*/
|
|
34
|
-
export async function tryServeFile(filePath: string): Promise<Response | null> {
|
|
35
|
-
try {
|
|
36
|
-
const data = await readFile(filePath);
|
|
37
|
-
const ext = filePath.split('.').pop()?.toLowerCase() ?? '';
|
|
38
|
-
const contentType = MIME[ext] ?? 'application/octet-stream';
|
|
39
|
-
return new Response(data as BodyInit, { headers: { 'Content-Type': contentType } });
|
|
40
|
-
} catch {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
}
|
package/src/utils/template.html
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { ServerContext } from './serve';
|
|
2
|
-
import type { HadarsOptions, HadarsRequest } from "../types/hadars";
|
|
3
|
-
|
|
4
|
-
type UpgradeHandle = (req: HadarsRequest, ctx: ServerContext) => boolean;
|
|
5
|
-
|
|
6
|
-
export const upgradeHandler = (options: HadarsOptions): UpgradeHandle | null => {
|
|
7
|
-
const { wsPath = '/ws' } = options;
|
|
8
|
-
|
|
9
|
-
if (options.websocket) {
|
|
10
|
-
return (req: HadarsRequest, ctx: ServerContext) => {
|
|
11
|
-
if (req.pathname === wsPath) {
|
|
12
|
-
return ctx.upgrade(req);
|
|
13
|
-
}
|
|
14
|
-
return false;
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return null;
|
|
19
|
-
};
|