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/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, '<');
|
|
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
|
-
};
|