hadars 0.1.1
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 +21 -0
- package/README.md +118 -0
- package/cli-bun.ts +13 -0
- package/cli-lib.ts +203 -0
- package/cli.ts +13 -0
- package/dist/cli.js +1441 -0
- package/dist/index.cjs +303 -0
- package/dist/index.d.ts +160 -0
- package/dist/index.js +263 -0
- package/dist/loader.cjs +34 -0
- package/dist/ssr-render-worker.js +92 -0
- package/dist/ssr-watch.js +345 -0
- package/dist/template.html +11 -0
- package/dist/utils/clientScript.tsx +58 -0
- package/index.ts +15 -0
- package/package.json +99 -0
- package/src/build.ts +716 -0
- package/src/index.tsx +41 -0
- package/src/ssr-render-worker.ts +138 -0
- package/src/ssr-watch.ts +56 -0
- package/src/types/global.d.ts +5 -0
- package/src/types/ninety.ts +116 -0
- package/src/utils/Head.tsx +357 -0
- package/src/utils/clientScript.tsx +58 -0
- package/src/utils/cookies.ts +16 -0
- package/src/utils/loadModule.ts +4 -0
- package/src/utils/loader.ts +41 -0
- package/src/utils/proxyHandler.tsx +101 -0
- package/src/utils/request.tsx +9 -0
- package/src/utils/response.tsx +198 -0
- package/src/utils/rspack.ts +359 -0
- package/src/utils/runtime.ts +19 -0
- package/src/utils/serve.ts +140 -0
- package/src/utils/staticFile.ts +48 -0
- package/src/utils/template.html +11 -0
- package/src/utils/upgradeRequest.tsx +19 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { isBun, isDeno } from './runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal server context passed to every fetch handler invocation.
|
|
5
|
+
* On Bun, `upgrade` performs a real WebSocket upgrade.
|
|
6
|
+
* On all other runtimes it is a no-op that always returns false.
|
|
7
|
+
*/
|
|
8
|
+
export interface ServerContext {
|
|
9
|
+
upgrade(req: Request): boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type FetchHandler = (
|
|
13
|
+
req: Request,
|
|
14
|
+
ctx: ServerContext,
|
|
15
|
+
) => Promise<Response | undefined> | Response | undefined;
|
|
16
|
+
|
|
17
|
+
/** Converts a Node.js Readable stream to a Web ReadableStream<Uint8Array>. */
|
|
18
|
+
function nodeReadableToWebStream(readable: NodeJS.ReadableStream): ReadableStream<Uint8Array> {
|
|
19
|
+
const enc = new TextEncoder();
|
|
20
|
+
return new ReadableStream<Uint8Array>({
|
|
21
|
+
start(controller) {
|
|
22
|
+
readable.on('data', (chunk: Buffer | string) => {
|
|
23
|
+
controller.enqueue(
|
|
24
|
+
typeof chunk === 'string'
|
|
25
|
+
? enc.encode(chunk)
|
|
26
|
+
: new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength),
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
readable.on('end', () => controller.close());
|
|
30
|
+
readable.on('error', (err) => controller.error(err));
|
|
31
|
+
},
|
|
32
|
+
cancel() {
|
|
33
|
+
(readable as any).destroy?.();
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const noopCtx: ServerContext = { upgrade: () => false };
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Starts an HTTP server on the given port using the best available runtime API:
|
|
42
|
+
* - **Bun**: `Bun.serve()` — full WebSocket support via the `websocket` option
|
|
43
|
+
* - **Deno**: `Deno.serve()`
|
|
44
|
+
* - **Node.js**: `node:http` `createServer()` with a Web-Fetch bridge
|
|
45
|
+
*
|
|
46
|
+
* The `fetchHandler` may return `undefined` to signal that the response was
|
|
47
|
+
* handled out-of-band (e.g. a Bun WebSocket upgrade).
|
|
48
|
+
*/
|
|
49
|
+
export async function serve(
|
|
50
|
+
port: number,
|
|
51
|
+
fetchHandler: FetchHandler,
|
|
52
|
+
/** Bun WebSocketHandler — ignored on Deno and Node.js. */
|
|
53
|
+
websocket?: unknown,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
// ── Bun ────────────────────────────────────────────────────────────────
|
|
56
|
+
if (isBun) {
|
|
57
|
+
(globalThis as any).Bun.serve({
|
|
58
|
+
port,
|
|
59
|
+
websocket,
|
|
60
|
+
async fetch(req: Request, server: any) {
|
|
61
|
+
const ctx: ServerContext = { upgrade: (r) => server.upgrade(r) };
|
|
62
|
+
// Returning undefined from a Bun fetch handler means the
|
|
63
|
+
// request was handled as a WebSocket upgrade.
|
|
64
|
+
return (await fetchHandler(req, ctx)) ?? undefined;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Deno ───────────────────────────────────────────────────────────────
|
|
71
|
+
// Deno 2.x changed the signature to options-first. Use the single-object
|
|
72
|
+
// form { port, handler } which is stable across Deno 1.x and 2.x.
|
|
73
|
+
if (isDeno) {
|
|
74
|
+
(globalThis as any).Deno.serve({
|
|
75
|
+
port,
|
|
76
|
+
handler: async (req: Request) => {
|
|
77
|
+
const res = await fetchHandler(req, noopCtx);
|
|
78
|
+
return res ?? new Response('Not Found', { status: 404 });
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Node.js ────────────────────────────────────────────────────────────
|
|
85
|
+
const { createServer } = await import('node:http');
|
|
86
|
+
|
|
87
|
+
const server = createServer(async (nodeReq, nodeRes) => {
|
|
88
|
+
try {
|
|
89
|
+
// Collect body for non-GET/HEAD requests
|
|
90
|
+
const chunks: Buffer[] = [];
|
|
91
|
+
if (!['GET', 'HEAD'].includes(nodeReq.method ?? 'GET')) {
|
|
92
|
+
for await (const chunk of nodeReq) {
|
|
93
|
+
chunks.push(chunk as Buffer);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const body = chunks.length > 0 ? Buffer.concat(chunks) : undefined;
|
|
97
|
+
|
|
98
|
+
const url = `http://localhost:${port}${nodeReq.url ?? '/'}`;
|
|
99
|
+
const reqInit: RequestInit & { duplex?: string } = {
|
|
100
|
+
method: nodeReq.method ?? 'GET',
|
|
101
|
+
headers: new Headers(nodeReq.headers as Record<string, string>),
|
|
102
|
+
};
|
|
103
|
+
if (body) {
|
|
104
|
+
reqInit.body = body;
|
|
105
|
+
reqInit.duplex = 'half';
|
|
106
|
+
}
|
|
107
|
+
const req = new Request(url, reqInit);
|
|
108
|
+
|
|
109
|
+
const res = await fetchHandler(req, noopCtx);
|
|
110
|
+
const response = res ?? new Response('Not Found', { status: 404 });
|
|
111
|
+
|
|
112
|
+
const headers: Record<string, string> = {};
|
|
113
|
+
response.headers.forEach((v, k) => { headers[k] = v; });
|
|
114
|
+
nodeRes.writeHead(response.status, headers);
|
|
115
|
+
|
|
116
|
+
if (response.body) {
|
|
117
|
+
const reader = response.body.getReader();
|
|
118
|
+
while (true) {
|
|
119
|
+
const { done, value } = await reader.read();
|
|
120
|
+
if (done) break;
|
|
121
|
+
await new Promise<void>((resolve, reject) =>
|
|
122
|
+
nodeRes.write(value, (err) => (err ? reject(err) : resolve())),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error('[hadars] request error', err);
|
|
128
|
+
if (!nodeRes.headersSent) nodeRes.writeHead(500);
|
|
129
|
+
} finally {
|
|
130
|
+
nodeRes.end();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await new Promise<void>((resolve, reject) => {
|
|
135
|
+
server.listen(port, () => resolve());
|
|
136
|
+
server.once('error', reject);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { nodeReadableToWebStream };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFile, stat } 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
|
+
await stat(filePath);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const data = await readFile(filePath);
|
|
42
|
+
const ext = filePath.split('.').pop()?.toLowerCase() ?? '';
|
|
43
|
+
const contentType = MIME[ext] ?? 'application/octet-stream';
|
|
44
|
+
return new Response(data, { headers: { 'Content-Type': contentType } });
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ServerContext } from './serve';
|
|
2
|
+
import type { HadarsOptions, HadarsRequest } from "../types/ninety";
|
|
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
|
+
};
|