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.
Files changed (58) hide show
  1. package/dist/{chunk-TV37IMRB.js → chunk-2TMQUXFL.js} +10 -10
  2. package/dist/{chunk-2J2L2H3H.js → chunk-NYLXE7T7.js} +6 -6
  3. package/dist/{chunk-OS3V4CPN.js → chunk-OZUZS2PD.js} +4 -4
  4. package/dist/cli.js +462 -496
  5. package/dist/cloudflare.cjs +11 -11
  6. package/dist/cloudflare.js +3 -3
  7. package/dist/index.d.cts +8 -4
  8. package/dist/index.d.ts +8 -4
  9. package/dist/lambda.cjs +11 -11
  10. package/dist/lambda.js +7 -7
  11. package/dist/loader.cjs +90 -54
  12. package/dist/slim-react/index.cjs +13 -13
  13. package/dist/slim-react/index.js +2 -2
  14. package/dist/slim-react/jsx-runtime.cjs +2 -4
  15. package/dist/slim-react/jsx-runtime.js +1 -1
  16. package/dist/ssr-render-worker.js +174 -161
  17. package/dist/ssr-watch.js +40 -74
  18. package/package.json +8 -10
  19. package/cli-lib.ts +0 -676
  20. package/cli.ts +0 -36
  21. package/index.ts +0 -17
  22. package/src/build.ts +0 -805
  23. package/src/cloudflare.ts +0 -140
  24. package/src/index.tsx +0 -41
  25. package/src/lambda.ts +0 -287
  26. package/src/slim-react/context.ts +0 -55
  27. package/src/slim-react/dispatcher.ts +0 -87
  28. package/src/slim-react/hooks.ts +0 -137
  29. package/src/slim-react/index.ts +0 -232
  30. package/src/slim-react/jsx-runtime.ts +0 -7
  31. package/src/slim-react/jsx.ts +0 -53
  32. package/src/slim-react/render.ts +0 -1101
  33. package/src/slim-react/renderContext.ts +0 -294
  34. package/src/slim-react/types.ts +0 -33
  35. package/src/source/context.ts +0 -113
  36. package/src/source/graphiql.ts +0 -101
  37. package/src/source/inference.ts +0 -260
  38. package/src/source/runner.ts +0 -138
  39. package/src/source/store.ts +0 -50
  40. package/src/ssr-render-worker.ts +0 -116
  41. package/src/ssr-watch.ts +0 -62
  42. package/src/static.ts +0 -109
  43. package/src/types/global.d.ts +0 -5
  44. package/src/types/hadars.ts +0 -350
  45. package/src/utils/Head.tsx +0 -462
  46. package/src/utils/clientScript.tsx +0 -71
  47. package/src/utils/cookies.ts +0 -16
  48. package/src/utils/loader.ts +0 -335
  49. package/src/utils/proxyHandler.tsx +0 -104
  50. package/src/utils/request.tsx +0 -9
  51. package/src/utils/response.tsx +0 -141
  52. package/src/utils/rspack.ts +0 -467
  53. package/src/utils/runtime.ts +0 -19
  54. package/src/utils/serve.ts +0 -155
  55. package/src/utils/ssrHandler.ts +0 -239
  56. package/src/utils/staticFile.ts +0 -43
  57. package/src/utils/template.html +0 -11
  58. package/src/utils/upgradeRequest.tsx +0 -19
package/src/build.ts DELETED
@@ -1,805 +0,0 @@
1
- import { createProxyHandler } from "./utils/proxyHandler";
2
- import { parseRequest } from "./utils/request";
3
- import { upgradeHandler } from "./utils/upgradeRequest";
4
- import { getReactResponse } from "./utils/response";
5
- import { createClientCompiler, compileEntry } from "./utils/rspack";
6
- import { serve, nodeReadableToWebStream } from "./utils/serve";
7
- import { tryServeFile } from "./utils/staticFile";
8
- import { isBun, isDeno, isNode } from "./utils/runtime";
9
- import { RspackDevServer } from "@rspack/dev-server";
10
- import pathMod from "node:path";
11
- import { fileURLToPath, pathToFileURL } from 'node:url';
12
- import crypto from 'node:crypto';
13
- import fs from 'node:fs/promises';
14
- import { existsSync } from 'node:fs';
15
- import os from 'node:os';
16
- import { spawn } from 'node:child_process';
17
- import cluster from 'node:cluster';
18
- import type { HadarsEntryModule, HadarsOptions, HadarsProps } from "./types/hadars";
19
- import {
20
- buildSsrResponse, makePrecontentHtmlGetter,
21
- type CacheFetchHandler, createRenderCache,
22
- } from './utils/ssrHandler';
23
- import { runSources } from './source/runner';
24
- import { buildSchemaExecutor } from './source/inference';
25
- import { createGraphiqlHandler, GRAPHQL_PATH } from './source/graphiql';
26
-
27
- /**
28
- * Reads an HTML template, processes any `<style>` blocks through PostCSS
29
- * (using the project's postcss.config.js), writes the result to a temp file,
30
- * and returns the temp file path. If there are no `<style>` blocks the
31
- * original path is returned unchanged.
32
- */
33
- async function processHtmlTemplate(templatePath: string): Promise<string> {
34
- const html = await fs.readFile(templatePath, 'utf-8');
35
-
36
- const styleRegex = /<style([^>]*)>([\s\S]*?)<\/style>/gi;
37
- const matches: Array<{ full: string; attrs: string; css: string }> = [];
38
- let m: RegExpExecArray | null;
39
- while ((m = styleRegex.exec(html)) !== null) {
40
- matches.push({ full: m[0]!, attrs: m[1] ?? '', css: m[2] ?? '' });
41
- }
42
- if (matches.length === 0) return templatePath;
43
-
44
- await ensureHadarsTmpDir();
45
-
46
- // Cache by content hash — same template content → skip Tailwind re-scan on restart.
47
- const sourceHash = crypto.createHash('md5').update(html).digest('hex').slice(0, 8);
48
- const cachedPath = pathMod.join(HADARS_TMP_DIR, `template-${sourceHash}.html`);
49
- try {
50
- await fs.access(cachedPath);
51
- return cachedPath; // cache hit
52
- } catch { /* cache miss — process below */ }
53
-
54
- const { default: postcss } = await import('postcss');
55
- let plugins: any[] = [];
56
- try {
57
- const { default: loadConfig } = await import('postcss-load-config' as any);
58
- const config = await loadConfig({}, process.cwd());
59
- plugins = (config as any).plugins ?? [];
60
- } catch {
61
- // No postcss config found — process without plugins (passthrough)
62
- }
63
-
64
- let processedHtml = html;
65
- for (const { full, attrs, css } of matches) {
66
- try {
67
- const result = await postcss(plugins).process(css, { from: templatePath });
68
- processedHtml = processedHtml.replace(full, `<style${attrs}>${result.css}</style>`);
69
- } catch (err) {
70
- console.warn('[hadars] PostCSS error processing <style> block in HTML template:', err);
71
- }
72
- }
73
-
74
- await fs.writeFile(cachedPath, processedHtml);
75
- return cachedPath;
76
- }
77
-
78
- // Round-robin thread pool for SSR rendering — used on Bun/Deno where
79
- // node:cluster is not available but node:worker_threads is.
80
-
81
- import type { SerializableRequest } from './ssr-render-worker';
82
-
83
- type PendingRenderFull = {
84
- kind: 'renderFull';
85
- resolve: (result: { html: string; headHtml: string; status: number }) => void;
86
- reject: (err: Error) => void;
87
- };
88
- type PendingEntry = PendingRenderFull;
89
-
90
- class RenderWorkerPool {
91
- private workers: any[] = [];
92
- private pending = new Map<number, PendingEntry>();
93
- // Track which pending IDs were dispatched to each worker so we can reject
94
- // them when that worker crashes.
95
- private workerPending = new Map<any, Set<number>>();
96
- private nextId = 0;
97
- private rrIndex = 0;
98
- private _Worker: any = null;
99
- private _workerPath = '';
100
- private _ssrBundlePath = '';
101
-
102
- constructor(workerPath: string, size: number, ssrBundlePath: string) {
103
- // Dynamically import Worker so this class can be defined at module load
104
- // time without a top-level await.
105
- this._init(workerPath, size, ssrBundlePath);
106
- }
107
-
108
- private _init(workerPath: string, size: number, ssrBundlePath: string) {
109
- this._workerPath = workerPath;
110
- this._ssrBundlePath = ssrBundlePath;
111
- import('node:worker_threads').then(({ Worker }) => {
112
- this._Worker = Worker;
113
- for (let i = 0; i < size; i++) this._spawnWorker();
114
- }).catch(err => {
115
- console.error('[hadars] Failed to initialise render worker pool:', err);
116
- });
117
- }
118
-
119
- private _spawnWorker() {
120
- if (!this._Worker) return;
121
- const w = new this._Worker(this._workerPath, { workerData: { ssrBundlePath: this._ssrBundlePath } });
122
- this.workerPending.set(w, new Set());
123
- w.on('message', (msg: any) => {
124
- const { id, html, headHtml, status, error } = msg;
125
- const p = this.pending.get(id);
126
- if (!p) return;
127
- this.pending.delete(id);
128
- this.workerPending.get(w)?.delete(id);
129
- if (error) p.reject(new Error(error));
130
- else p.resolve({ html, headHtml, status });
131
- });
132
- w.on('error', (err: Error) => {
133
- console.error('[hadars] Render worker error:', err);
134
- this._handleWorkerDeath(w, err);
135
- });
136
- w.on('exit', (code: number) => {
137
- if (code !== 0) {
138
- console.error(`[hadars] Render worker exited with code ${code}`);
139
- this._handleWorkerDeath(w, new Error(`Render worker exited with code ${code}`));
140
- }
141
- });
142
- this.workers.push(w);
143
- }
144
-
145
- private _handleWorkerDeath(w: any, err: Error) {
146
- const idx = this.workers.indexOf(w);
147
- if (idx !== -1) this.workers.splice(idx, 1);
148
-
149
- const ids = this.workerPending.get(w);
150
- if (ids) {
151
- for (const id of ids) {
152
- const p = this.pending.get(id);
153
- if (p) {
154
- this.pending.delete(id);
155
- p.reject(err);
156
- }
157
- }
158
- this.workerPending.delete(w);
159
- }
160
-
161
- // Spawn a replacement to keep the pool at full capacity.
162
- console.log('[hadars] Spawning replacement render worker');
163
- this._spawnWorker();
164
- }
165
-
166
- private nextWorker(): any | undefined {
167
- if (this.workers.length === 0) return undefined;
168
- const w = this.workers[this.rrIndex % this.workers.length];
169
- this.rrIndex++;
170
- return w;
171
- }
172
-
173
- /** Run the full SSR lifecycle in a worker thread. Returns html, headHtml, status. */
174
- renderFull(req: SerializableRequest): Promise<{ html: string; headHtml: string; status: number }> {
175
- return new Promise((resolve, reject) => {
176
- const w = this.nextWorker();
177
- if (!w) { reject(new Error('[hadars] No render workers available')); return; }
178
- const id = this.nextId++;
179
- this.pending.set(id, { kind: 'renderFull', resolve, reject });
180
- this.workerPending.get(w)?.add(id);
181
- try {
182
- w.postMessage({ id, type: 'renderFull', streaming: false, request: req });
183
- } catch (err) {
184
- this.pending.delete(id);
185
- this.workerPending.get(w)?.delete(id);
186
- reject(err);
187
- }
188
- });
189
- }
190
-
191
- async terminate(): Promise<void> {
192
- await Promise.all(this.workers.map((w: any) => w.terminate()));
193
- }
194
- }
195
-
196
- /** Serialize a HadarsRequest into a structure-clonable object for postMessage. */
197
- async function serializeRequest(req: any): Promise<SerializableRequest> {
198
- const isGetOrHead = ['GET', 'HEAD'].includes(req.method ?? 'GET');
199
- let body: Uint8Array | null = null;
200
- if (!isGetOrHead) {
201
- try {
202
- body = new Uint8Array(await req.arrayBuffer());
203
- } catch {
204
- // Body already consumed upstream (e.g. by options.fetch returning undefined
205
- // after reading the body). Proceed without body — SSR does not need it.
206
- }
207
- }
208
- const headers: Record<string, string> = {};
209
- (req.headers as Headers).forEach((v: string, k: string) => { headers[k] = v; });
210
- return {
211
- url: req.url,
212
- method: req.method ?? 'GET',
213
- headers,
214
- body,
215
- pathname: req.pathname,
216
- search: req.search,
217
- location: req.location,
218
- cookies: req.cookies,
219
- };
220
- }
221
-
222
- interface HadarsRuntimeOptions extends HadarsOptions {
223
- mode: "development" | "production";
224
- }
225
-
226
- const SSR_FILENAME = 'index.ssr.js';
227
- const __dirname = process.cwd();
228
-
229
- type Mode = "development" | "production";
230
-
231
- const getSuffix = (mode: Mode) => mode === 'development' ? `?v=${Date.now()}` : '';
232
-
233
- const HadarsFolder = './.hadars';
234
- const StaticPath = `${HadarsFolder}/static`;
235
- // Dedicated temp directory — keeps all hadars temp files out of the root of
236
- // os.tmpdir() so rspack's file watcher doesn't traverse unrelated system files
237
- // (e.g. Steam/Chrome shared-memory device files) in that directory.
238
- const HADARS_TMP_DIR = pathMod.join(os.tmpdir(), 'hadars');
239
- const ensureHadarsTmpDir = () => fs.mkdir(HADARS_TMP_DIR, { recursive: true });
240
-
241
- const validateOptions = (options: HadarsRuntimeOptions) => {
242
- if (!options.entry) {
243
- throw new Error("Entry file is required");
244
- }
245
- if (options.mode !== 'development' && options.mode !== 'production') {
246
- throw new Error("Mode must be either 'development' or 'production'");
247
- }
248
- };
249
-
250
- /**
251
- * Resolves the SSR worker script and the command used to run it.
252
- *
253
- * Four modes:
254
- * 1. Bun (source) — `bun ssr-watch.ts`
255
- * 2. Deno (source) — `deno run --allow-all ssr-watch.ts`
256
- * 3. Node.js (source) — tsx/ts-node detected via execArgv; used when the
257
- * caller itself was launched by a TS runner (e.g. `npx tsx cli.ts dev`)
258
- * 4. Node.js (compiled) — `node ssr-watch.js` (post `npm run build:cli`)
259
- */
260
- const resolveWorkerCmd = (packageDir: string): string[] => {
261
- const tsPath = pathMod.resolve(packageDir, 'ssr-watch.ts');
262
- const jsPath = pathMod.resolve(packageDir, 'ssr-watch.js');
263
-
264
- if (isBun && existsSync(tsPath)) {
265
- return ['bun', tsPath];
266
- }
267
-
268
- if (isDeno && existsSync(tsPath)) {
269
- return ['deno', 'run', '--allow-all', tsPath];
270
- }
271
-
272
- // Detect if the current process was launched by a Node.js TypeScript runner
273
- // (tsx, ts-node). Modern tsx injects itself via --import into execArgv;
274
- // older versions appear in argv[1]. ts-node works similarly.
275
- if (existsSync(tsPath)) {
276
- const allArgs = [...process.execArgv, process.argv[1] ?? ''];
277
- const hasTsx = allArgs.some(a => a.includes('tsx'));
278
- const hasTsNode = allArgs.some(a => a.includes('ts-node'));
279
- if (hasTsx) return ['tsx', tsPath];
280
- if (hasTsNode) return ['ts-node', tsPath];
281
- }
282
-
283
- if (existsSync(jsPath)) {
284
- return ['node', jsPath];
285
- }
286
-
287
- throw new Error(
288
- `[hadars] SSR worker not found. Expected:\n` +
289
- ` ${jsPath}\n` +
290
- `Run "npm run build:cli" to compile it, or launch hadars via a TypeScript runner:\n` +
291
- ` npx tsx cli.ts dev`
292
- );
293
- };
294
-
295
- export const dev = async (options: HadarsRuntimeOptions) => {
296
-
297
- // clean .hadars
298
- await fs.rm(HadarsFolder, { recursive: true, force: true });
299
-
300
- let { port = 9090, baseURL = '' } = options;
301
-
302
- console.log(`Starting Hadars on port ${port}`);
303
-
304
- validateOptions(options);
305
- const handleProxy = createProxyHandler(options);
306
- const handleWS = upgradeHandler(options);
307
- const handler = options.fetch;
308
-
309
- // Run source plugins and set up GraphiQL if config.sources is present.
310
- let handleGraphiql: ((req: Request) => Promise<Response | undefined>) | null = null;
311
- let devStaticCtx: { graphql: import('./types/hadars').GraphQLExecutor } | undefined;
312
- if (options.sources && options.sources.length > 0) {
313
- console.log(`[hadars] Running ${options.sources.length} source plugin(s)…`);
314
- try {
315
- const store = await runSources(options.sources);
316
- const executor = await buildSchemaExecutor(store);
317
- if (executor) {
318
- devStaticCtx = { graphql: executor };
319
- handleGraphiql = createGraphiqlHandler(executor);
320
- console.log(`[hadars] GraphiQL available at http://localhost:${port}${GRAPHQL_PATH}`);
321
- } else {
322
- console.warn('[hadars] `graphql` package not found — GraphiQL disabled. Run: npm install graphql');
323
- }
324
- } catch (err) {
325
- console.error('[hadars] Source plugin error:', err);
326
- }
327
- }
328
-
329
- const entry = pathMod.resolve(__dirname, options.entry);
330
- const hmrPort = options.hmrPort ?? port + 1;
331
-
332
- // prepare client script once (we will compile into StaticPath)
333
- const packageDir = pathMod.dirname(fileURLToPath(import.meta.url));
334
- const clientScriptPath = pathMod.resolve(packageDir, 'utils', 'clientScript.tsx');
335
-
336
- let clientScript = '';
337
- try {
338
- clientScript = (await fs.readFile(clientScriptPath, 'utf-8'))
339
- .replace('$_MOD_PATH$', entry + getSuffix(options.mode));
340
- }
341
- catch (err) {
342
- console.error("Failed to read client script from package dist, falling back to src", err);
343
- throw err;
344
- }
345
-
346
- await ensureHadarsTmpDir();
347
- const tmpFilePath = pathMod.join(HADARS_TMP_DIR, `client-${Date.now()}.tsx`);
348
- await fs.writeFile(tmpFilePath, clientScript);
349
-
350
- // SSR live-reload id to force re-import
351
- let ssrBuildId = crypto.randomBytes(4).toString('hex');
352
-
353
- // Pre-process the HTML template's <style> blocks through PostCSS (e.g. Tailwind).
354
- const resolvedHtmlTemplate = options.htmlTemplate
355
- ? await processHtmlTemplate(pathMod.resolve(__dirname, options.htmlTemplate))
356
- : undefined;
357
-
358
- // Start rspack-dev-server for the client bundle. It provides true React
359
- // Fast Refresh HMR: the browser's HMR runtime connects directly to the
360
- // dev server's WebSocket on hmrPort and receives module-level patches
361
- // without full page reloads. writeToDisk lets the server serve the
362
- // initial index.js and out.html from disk.
363
- const clientCompiler = createClientCompiler(tmpFilePath, {
364
- target: 'web',
365
- output: {
366
- filename: "index.js",
367
- path: pathMod.resolve(__dirname, StaticPath),
368
- },
369
- base: baseURL,
370
- mode: 'development',
371
- swcPlugins: options.swcPlugins,
372
- define: options.define,
373
- moduleRules: options.moduleRules,
374
- plugins: options.plugins,
375
- postcssPlugins: options.postcssPlugins,
376
- reactMode: options.reactMode,
377
- htmlTemplate: resolvedHtmlTemplate,
378
- });
379
-
380
- const devServer = new RspackDevServer({
381
- port: hmrPort,
382
- hot: true,
383
- liveReload: false,
384
- client: {
385
- webSocketURL: `ws://localhost:${hmrPort}/ws`,
386
- },
387
- devMiddleware: {
388
- writeToDisk: true,
389
- },
390
- headers: { 'Access-Control-Allow-Origin': '*' },
391
- allowedHosts: 'all',
392
- }, clientCompiler as any);
393
-
394
- console.log(`Starting HMR dev server on port ${hmrPort}`);
395
-
396
- // Kick off client build — does NOT await here so SSR worker can start in parallel.
397
- let clientResolved = false;
398
- const clientBuildDone = new Promise<void>((resolve, reject) => {
399
- (clientCompiler as any).hooks.done.tap('initial-build', (stats: any) => {
400
- if (!clientResolved) {
401
- clientResolved = true;
402
- console.log(stats.toString({ colors: true }));
403
- resolve();
404
- }
405
- });
406
- devServer.start().catch(reject);
407
- });
408
-
409
- // Start SSR watcher in a separate process to avoid creating two rspack
410
- // compiler instances in the same process. We use node:child_process.spawn
411
- // which works on Bun, Node.js, and Deno (via compatibility layer).
412
- // Spawned immediately so it compiles in parallel with the client build above.
413
- const workerCmd = resolveWorkerCmd(packageDir);
414
- console.log('Spawning SSR worker:', workerCmd.join(' '), 'entry:', entry);
415
-
416
- const child = spawn(workerCmd[0]!, [
417
- ...workerCmd.slice(1),
418
- `--entry=${entry}`,
419
- `--outDir=${HadarsFolder}`,
420
- `--outFile=${SSR_FILENAME}`,
421
- `--base=${baseURL}`,
422
- ...(options.swcPlugins ? [`--swcPlugins=${JSON.stringify(options.swcPlugins)}`] : []),
423
- ...(options.define ? [`--define=${JSON.stringify(options.define)}`] : []),
424
- ...(options.moduleRules ? [`--moduleRules=${JSON.stringify(options.moduleRules, (_k, v) => v instanceof RegExp ? { __re: v.source, __flags: v.flags } : v)}`] : []),
425
- ], { stdio: 'pipe' });
426
- child.stdin?.end();
427
-
428
- // Ensure the SSR watcher child is killed when this process exits.
429
- const cleanupChild = () => { try { if (!child.killed) child.kill(); } catch {} };
430
- process.once('exit', cleanupChild);
431
- process.once('SIGINT', () => { cleanupChild(); process.exit(0); });
432
- process.once('SIGTERM', () => { cleanupChild(); process.exit(0); });
433
-
434
- // Convert Node.js Readable streams to Web ReadableStream so the rest of
435
- // the logic works identically across all runtimes.
436
- const stdoutWebStream = nodeReadableToWebStream(child.stdout!);
437
- const stderrWebStream = nodeReadableToWebStream(child.stderr!);
438
-
439
- // Wait for worker to emit the initial build completion marker.
440
- const marker = 'ssr-watch: initial-build-complete';
441
- const rebuildMarker = 'ssr-watch: SSR rebuilt';
442
- const decoder = new TextDecoder();
443
- // Hoist so the async continuation loop below can keep using it.
444
- let stdoutReader: ReadableStreamDefaultReader<Uint8Array> | null = null;
445
- const ssrBuildDone = (async () => {
446
- let gotMarker = false;
447
- try {
448
- stdoutReader = stdoutWebStream.getReader();
449
- let buf = '';
450
- const start = Date.now();
451
- const timeoutMs = 20000;
452
- while (Date.now() - start < timeoutMs) {
453
- const { done, value } = await stdoutReader.read();
454
- if (done) { stdoutReader = null; break; }
455
- const chunk = decoder.decode(value, { stream: true });
456
- buf += chunk;
457
- try { process.stdout.write(chunk); } catch (e) { /* ignore */ }
458
- if (buf.includes(marker)) {
459
- gotMarker = true;
460
- break;
461
- }
462
- }
463
- if (!gotMarker) {
464
- console.warn('SSR worker did not signal initial build completion within timeout');
465
- }
466
- } catch (err) {
467
- console.error('Error reading SSR worker output', err);
468
- stdoutReader = null;
469
- }
470
- })();
471
-
472
- // Both builds run in parallel — this promise resolves when they're both done.
473
- // We do NOT await it here; the server starts immediately below so that the
474
- // port is bound right away. Incoming requests await this promise before
475
- // processing, so they hold in-flight and all resolve together once ready.
476
- const readyPromise = Promise.all([clientBuildDone, ssrBuildDone]);
477
-
478
- readyPromise.then(() => {
479
- // Continue reading stdout to forward logs and pick up SSR rebuild signals.
480
- if (stdoutReader) {
481
- const reader = stdoutReader as ReadableStreamDefaultReader<Uint8Array>;
482
- (async () => {
483
- try {
484
- while (true) {
485
- const { done, value } = await reader.read();
486
- if (done) break;
487
- const chunk = decoder.decode(value, { stream: true });
488
- try { process.stdout.write(chunk); } catch (e) { }
489
- if (chunk.includes(rebuildMarker)) {
490
- ssrBuildId = crypto.randomBytes(4).toString('hex');
491
- console.log('[hadars] SSR bundle updated, build id:', ssrBuildId);
492
- }
493
- }
494
- } catch (e) { }
495
- })();
496
- }
497
- });
498
-
499
- // Forward stderr asynchronously
500
- (async () => {
501
- try {
502
- const r = stderrWebStream.getReader();
503
- while (true) {
504
- const { done, value } = await r.read();
505
- if (done) break;
506
- try { process.stderr.write(decoder.decode(value)); } catch (e) { }
507
- }
508
- } catch (e) { }
509
- })();
510
-
511
- const getPrecontentHtml = makePrecontentHtmlGetter(
512
- readyPromise.then(() => fs.readFile(pathMod.join(__dirname, StaticPath, 'out.html'), 'utf-8'))
513
- );
514
-
515
- await serve(port, async (req, ctx) => {
516
- // Hold requests until both builds are ready. Once resolved this is a no-op.
517
- await readyPromise;
518
- const request = parseRequest(req);
519
- if (handler) {
520
- const res = await handler(request);
521
- if (res) return res;
522
- }
523
- if (handleWS && handleWS(request, ctx)) return undefined;
524
-
525
- if (handleGraphiql) {
526
- const graphiqlRes = await handleGraphiql(req);
527
- if (graphiqlRes) return graphiqlRes;
528
- }
529
-
530
- const proxied = await handleProxy(request);
531
- if (proxied) return proxied;
532
-
533
- const url = new URL(request.url);
534
- const path = url.pathname;
535
-
536
- // static files in the hadars output folder
537
- const staticRes = await tryServeFile(pathMod.join(__dirname, StaticPath, path));
538
- if (staticRes) return staticRes;
539
-
540
- // project-level static/ directory (explicit paths only — never intercept root)
541
- const projectStaticPath = pathMod.resolve(process.cwd(), 'static');
542
- const projectRes = await tryServeFile(pathMod.join(projectStaticPath, path));
543
- if (projectRes) return projectRes;
544
-
545
- const ssrComponentPath = pathMod.join(__dirname, HadarsFolder, SSR_FILENAME);
546
- // Use a file: URL so the ?t= suffix is treated as a URL query string
547
- // (cache-busting key) rather than a literal filename character on Linux.
548
- const importPath = pathToFileURL(ssrComponentPath).href + `?t=${ssrBuildId}`;
549
-
550
- try {
551
- const {
552
- default: Component,
553
- getInitProps,
554
- getFinalProps,
555
- } = (await import(importPath)) as HadarsEntryModule<any>;
556
-
557
- // Expose the executor globally so useGraphQL() in components can reach it.
558
- (globalThis as any).__hadarsGraphQL = devStaticCtx?.graphql;
559
-
560
- const { head, status, getAppBody, finalize } = await getReactResponse(request, {
561
- document: {
562
- body: Component as React.FC<HadarsProps<object>>,
563
- lang: 'en',
564
- getInitProps,
565
- getFinalProps,
566
- },
567
- staticCtx: devStaticCtx,
568
- });
569
-
570
- // Content negotiation: if the client only accepts JSON (client-side
571
- // navigation via useServerData), return the resolved data map as JSON
572
- // instead of a full HTML page. The same auth context applies — cookies
573
- // and headers are forwarded unchanged, so no new attack surface is created.
574
- if (request.headers.get('Accept') === 'application/json') {
575
- const { clientProps } = await finalize();
576
- const serverData = (clientProps as any).__serverData ?? {};
577
- return new Response(JSON.stringify({ serverData }), {
578
- status,
579
- headers: { 'Content-Type': 'application/json; charset=utf-8' },
580
- });
581
- }
582
-
583
- return buildSsrResponse(head, status, getAppBody, finalize, getPrecontentHtml);
584
- } catch (err: any) {
585
- console.error('[hadars] SSR render error:', err);
586
- options.onError?.(err, request)?.catch?.(() => {});
587
- const msg = (err?.stack ?? err?.message ?? String(err)).replace(/</g, '&lt;');
588
- return new Response(`<!doctype html><pre style="white-space:pre-wrap">${msg}</pre>`, {
589
- status: 500,
590
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
591
- });
592
- }
593
- }, options.websocket);
594
- };
595
-
596
- export const build = async (options: HadarsRuntimeOptions) => {
597
- validateOptions(options);
598
-
599
- const entry = pathMod.resolve(__dirname, options.entry);
600
-
601
- // prepare client script
602
- const packageDir = pathMod.dirname(fileURLToPath(import.meta.url));
603
- const clientScriptPath = pathMod.resolve(packageDir, 'utils', 'clientScript.js');
604
- let clientScript = '';
605
- try {
606
- clientScript = (await fs.readFile(clientScriptPath, 'utf-8'))
607
- .replace('$_MOD_PATH$', entry + getSuffix(options.mode));
608
- } catch (err) {
609
- const srcClientPath = pathMod.resolve(packageDir, 'utils', 'clientScript.tsx');
610
- clientScript = (await fs.readFile(srcClientPath, 'utf-8'))
611
- .replace('$_MOD_PATH$', entry + `?v=${Date.now()}`);
612
- }
613
-
614
- await ensureHadarsTmpDir();
615
- const tmpFilePath = pathMod.join(HADARS_TMP_DIR, `client-${Date.now()}.tsx`);
616
- await fs.writeFile(tmpFilePath, clientScript);
617
-
618
- // Pre-process the HTML template's <style> blocks through PostCSS (e.g. Tailwind).
619
- const resolvedHtmlTemplate = options.htmlTemplate
620
- ? await processHtmlTemplate(pathMod.resolve(__dirname, options.htmlTemplate))
621
- : undefined;
622
-
623
- // Compile client and SSR bundles in parallel — they write to different
624
- // output directories and use different entry files, so they are fully
625
- // independent and safe to run concurrently.
626
- console.log("Building client and server bundles in parallel...");
627
- await Promise.all([
628
- compileEntry(tmpFilePath, {
629
- target: 'web',
630
- output: {
631
- // Content hash: filename is stable when code is unchanged → better browser/CDN cache.
632
- filename: 'index.[contenthash:8].js',
633
- path: pathMod.resolve(__dirname, StaticPath),
634
- },
635
- base: options.baseURL,
636
- mode: 'production',
637
- swcPlugins: options.swcPlugins,
638
- define: options.define,
639
- moduleRules: options.moduleRules,
640
- plugins: options.plugins,
641
- postcssPlugins: options.postcssPlugins,
642
- optimization: options.optimization,
643
- reactMode: options.reactMode,
644
- htmlTemplate: resolvedHtmlTemplate,
645
- }),
646
- compileEntry(pathMod.resolve(__dirname, options.entry), {
647
- output: {
648
- iife: false,
649
- filename: SSR_FILENAME,
650
- path: pathMod.resolve(__dirname, HadarsFolder),
651
- publicPath: '',
652
- library: { type: 'module' },
653
- },
654
- base: options.baseURL,
655
- target: 'node',
656
- mode: 'production',
657
- swcPlugins: options.swcPlugins,
658
- define: options.define,
659
- moduleRules: options.moduleRules,
660
- plugins: options.plugins,
661
- postcssPlugins: options.postcssPlugins,
662
- }),
663
- ]);
664
- await fs.rm(tmpFilePath);
665
- console.log("Build complete.");
666
- };
667
-
668
- export const run = async (options: HadarsRuntimeOptions) => {
669
- validateOptions(options);
670
-
671
- let { port = 9090, workers = 1 } = options;
672
-
673
- // On Node.js, fork worker processes so every CPU core handles requests.
674
- // The primary process only manages the cluster; workers fall through to
675
- // the serve() call below. On Bun/Deno this is skipped — Bun has its own
676
- // multi-threaded I/O model and doesn't need OS-level process forking.
677
- if (isNode && workers > 1 && cluster.isPrimary) {
678
- console.log(`[hadars] Starting ${workers} worker processes on port ${port}`);
679
- for (let i = 0; i < workers; i++) {
680
- cluster.fork();
681
- }
682
- cluster.on('exit', (worker, code, signal) => {
683
- console.warn(`[hadars] Worker ${worker.process.pid} exited (${signal ?? code}), restarting...`);
684
- cluster.fork();
685
- });
686
- await new Promise(() => {}); // keep primary alive; workers handle requests
687
- return;
688
- }
689
-
690
- const handleProxy = createProxyHandler(options);
691
- const handleWS = upgradeHandler(options);
692
- const handler = options.fetch;
693
-
694
- console.log(`Starting Hadars (run) on port ${port}`);
695
-
696
- // On Bun/Deno, node:cluster is unavailable, so we use a worker_threads
697
- // render pool to parallelize the synchronous renderToString step instead.
698
- let renderPool: RenderWorkerPool | undefined;
699
- if (!isNode && workers > 1) {
700
- const packageDir = pathMod.dirname(fileURLToPath(import.meta.url));
701
- const workerJs = pathMod.resolve(packageDir, 'ssr-render-worker.js');
702
- const workerTs = pathMod.resolve(packageDir, 'ssr-render-worker.ts');
703
- const workerFile = existsSync(workerJs) ? workerJs : workerTs;
704
- const ssrBundlePath = pathMod.resolve(__dirname, HadarsFolder, SSR_FILENAME);
705
- renderPool = new RenderWorkerPool(workerFile, workers, ssrBundlePath);
706
- console.log(`[hadars] SSR render pool: ${workers} worker threads`);
707
- }
708
-
709
- const getPrecontentHtml = makePrecontentHtmlGetter(
710
- fs.readFile(pathMod.join(__dirname, StaticPath, 'out.html'), 'utf-8')
711
- );
712
-
713
- // Hoist and pre-import the SSR module at startup so the first request does
714
- // not pay the module parse/eval cost. The file: URL is stable for the life
715
- // of the process (no cache-busting needed in run mode).
716
- const componentPath = pathToFileURL(
717
- pathMod.resolve(__dirname, HadarsFolder, SSR_FILENAME)
718
- ).href;
719
- const ssrModulePromise = import(componentPath) as Promise<HadarsEntryModule<any>>;
720
-
721
- const runHandler: CacheFetchHandler = async (req, ctx) => {
722
- const request = parseRequest(req);
723
- if (handler) {
724
- const res = await handler(request);
725
- if (res) return res;
726
- }
727
- if (handleWS && handleWS(request, ctx)) return undefined;
728
-
729
- const proxied = await handleProxy(request);
730
- if (proxied) return proxied;
731
-
732
- const url = new URL(request.url);
733
- const path = url.pathname;
734
-
735
- // static files in the hadars output folder
736
- const staticRes = await tryServeFile(pathMod.join(__dirname, StaticPath, path));
737
- if (staticRes) return staticRes;
738
-
739
- // project-level static/ directory (explicit paths only — never intercept root)
740
- const projectStaticPath = pathMod.resolve(process.cwd(), 'static');
741
- const projectRes = await tryServeFile(pathMod.join(projectStaticPath, path));
742
- if (projectRes) return projectRes;
743
-
744
- // route-based fallback: try <path>/index.html
745
- const routeClean = path.replace(/(^\/|\/$)/g, '');
746
- if (routeClean) {
747
- const routeRes = await tryServeFile(
748
- pathMod.join(__dirname, StaticPath, routeClean, 'index.html')
749
- );
750
- if (routeRes) return routeRes;
751
- }
752
-
753
- try {
754
- const {
755
- default: Component,
756
- getInitProps,
757
- getFinalProps,
758
- } = await ssrModulePromise;
759
-
760
- if (renderPool && request.headers.get('Accept') !== 'application/json') {
761
- // Worker runs the full lifecycle — no non-serializable objects cross the thread boundary.
762
- const serialReq = await serializeRequest(request);
763
- const { html, headHtml: wHead, status: wStatus } = await renderPool.renderFull(serialReq);
764
- const [precontentHtml, postContent] = await getPrecontentHtml(wHead);
765
- return new Response(precontentHtml + html + postContent, {
766
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
767
- status: wStatus,
768
- });
769
- }
770
-
771
- const { head, status, getAppBody, finalize } = await getReactResponse(request, {
772
- document: {
773
- body: Component as React.FC<HadarsProps<object>>,
774
- lang: 'en',
775
- getInitProps,
776
- getFinalProps,
777
- },
778
- });
779
-
780
- // Content negotiation: if the client only accepts JSON (client-side
781
- // navigation via useServerData), return the resolved data map as JSON
782
- // instead of a full HTML page.
783
- if (request.headers.get('Accept') === 'application/json') {
784
- const { clientProps } = await finalize();
785
- const serverData = (clientProps as any).__serverData ?? {};
786
- return new Response(JSON.stringify({ serverData }), {
787
- status,
788
- headers: { 'Content-Type': 'application/json; charset=utf-8' },
789
- });
790
- }
791
-
792
- return buildSsrResponse(head, status, getAppBody, finalize, getPrecontentHtml);
793
- } catch (err: any) {
794
- console.error('[hadars] SSR render error:', err);
795
- options.onError?.(err, request)?.catch?.(() => {});
796
- return new Response('Internal Server Error', { status: 500 });
797
- }
798
- };
799
-
800
- await serve(
801
- port,
802
- options.cache ? createRenderCache(options.cache, runHandler) : runHandler,
803
- options.websocket,
804
- );
805
- };