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
@@ -1,467 +0,0 @@
1
- import rspack from "@rspack/core";
2
- import type { Configuration, RuleSetLoaderWithOptions, RuleSetRule } from "@rspack/core";
3
- import ReactRefreshPlugin from '@rspack/plugin-react-refresh';
4
- import path from 'node:path';
5
- import type { SwcPluginList } from '../types/hadars';
6
- import { fileURLToPath } from "node:url";
7
- import pathMod from "node:path";
8
- import { existsSync } from "node:fs";
9
-
10
- const __dirname = process.cwd();
11
- const packageDir = pathMod.dirname(fileURLToPath(import.meta.url));
12
- const clientScriptPath = pathMod.resolve(packageDir, 'template.html');
13
-
14
- // When running from compiled dist/cli.js the loader is pre-built as loader.cjs.
15
- // The .cjs extension forces CommonJS regardless of the package "type": "module".
16
- // When running from source (bun/tsx) it falls back to loader.ts.
17
- const loaderPath = existsSync(pathMod.resolve(packageDir, 'loader.cjs'))
18
- ? pathMod.resolve(packageDir, 'loader.cjs')
19
- : pathMod.resolve(packageDir, 'loader.ts');
20
-
21
- const getConfigBase = (mode: "development" | "production", isServerBuild = false): Omit<Configuration, "entry" | "output" | "plugins"> => {
22
- const isDev = mode === 'development';
23
- return {
24
- experiments: {
25
- css: true,
26
- outputModule: true,
27
- },
28
- resolve: {
29
- modules: [
30
- path.resolve(__dirname, 'node_modules'),
31
- // 'node_modules' (relative) enables the standard upward-traversal
32
- // resolution so rspack can find transitive deps (e.g. webpack-dev-server)
33
- // that live in a parent node_modules when running from a sub-project.
34
- 'node_modules',
35
- ],
36
- tsConfig: path.resolve(__dirname, 'tsconfig.json'),
37
- extensions: ['.tsx', '.ts', '.js', '.jsx'],
38
- },
39
- module: {
40
- rules: [
41
- {
42
- test: /\.css$/,
43
- use: [{ loader: "builtin:lightningcss-loader" }],
44
- type: "css",
45
- },
46
- {
47
- test: /\.svg$/i,
48
- issuer: /\.[jt]sx?$/,
49
- use: ['@svgr/webpack'],
50
- },
51
- {
52
- test: /\.m?jsx?$/,
53
- resolve: {
54
- fullySpecified: false,
55
- },
56
- exclude: [loaderPath],
57
- use: [
58
- // Transforms loadModule('./path') based on build target.
59
- // Runs before swc-loader (loaders execute right-to-left).
60
- {
61
- loader: loaderPath,
62
- options: { server: isServerBuild },
63
- },
64
- {
65
- loader: 'builtin:swc-loader',
66
- options: {
67
- jsc: {
68
- parser: {
69
- syntax: 'ecmascript',
70
- jsx: true,
71
- },
72
- transform: {
73
- react: {
74
- runtime: "automatic",
75
- development: isDev,
76
- refresh: isDev && !isServerBuild,
77
- },
78
- },
79
- },
80
- },
81
- },
82
- ],
83
- type: 'javascript/auto',
84
- },
85
- {
86
- test: /\.tsx?$/,
87
- resolve: {
88
- fullySpecified: false,
89
- },
90
- exclude: [loaderPath],
91
- use: [
92
- {
93
- loader: loaderPath,
94
- options: { server: isServerBuild },
95
- },
96
- {
97
- loader: 'builtin:swc-loader',
98
- options: {
99
- jsc: {
100
- parser: {
101
- syntax: 'typescript',
102
- tsx: true,
103
- },
104
- transform: {
105
- react: {
106
- runtime: "automatic",
107
- development: isDev,
108
- refresh: isDev && !isServerBuild,
109
- },
110
- },
111
- },
112
- },
113
- },
114
- ],
115
- type: 'javascript/auto',
116
- },
117
- ],
118
- },
119
- }
120
- }
121
-
122
- type EntryOutput = Configuration["output"];
123
-
124
- interface EntryOptions {
125
- target: Configuration["target"],
126
- output: EntryOutput,
127
- mode: "development" | "production",
128
- // optional swc plugins to pass to swc-loader
129
- swcPlugins?: SwcPluginList,
130
- // optional path to a custom HTML template (resolved relative to cwd)
131
- htmlTemplate?: string,
132
- // optional compile-time defines (e.g. { 'process.env.NODE_ENV': '"development"' })
133
- define?: Record<string, string>;
134
- base?: string;
135
- // optional rspack optimization overrides (production client builds only)
136
- optimization?: Record<string, unknown>;
137
- // additional module rules appended after the built-in rules
138
- moduleRules?: Record<string, any>[];
139
- // additional rspack/webpack-compatible plugins (applied after built-in plugins)
140
- plugins?: Array<{ apply(compiler: any): void }>;
141
- // PostCSS plugins to pass to postcss-loader (replaces the default builtin:lightningcss-loader).
142
- // Use this when you need PostCSS transforms such as Tailwind CSS v4 (@tailwindcss/postcss).
143
- postcssPlugins?: any[];
144
- // force React runtime mode independently of build mode (client only)
145
- reactMode?: 'development' | 'production';
146
- }
147
-
148
- const buildCompilerConfig = (
149
- entry: string,
150
- opts: EntryOptions,
151
- includeHotPlugin: boolean,
152
- ): Configuration => {
153
- const { base } = opts;
154
- const isDev = opts.mode === 'development';
155
- const isServerBuild = Boolean(
156
- (opts.output && typeof opts.output === 'object' && (opts.output.library || String(opts.output.filename || '').includes('ssr')))
157
- );
158
- const Config = getConfigBase(opts.mode, isServerBuild);
159
-
160
- // shallow-clone base config to avoid mutating shared Config while preserving RegExp and plugin instances
161
- const localConfig: any = {
162
- ...Config,
163
- module: {
164
- ...Config.module,
165
- rules: (Config.module && Array.isArray(Config.module.rules) ? Config.module.rules : []).map((r: any) => {
166
- // shallow copy each rule and its 'use' array/entries so we can mutate safely
167
- const nr: any = { ...r };
168
- if (r && Array.isArray(r.use)) {
169
- nr.use = r.use.map((u: any) => ({ ...(typeof u === 'object' ? u : { loader: u }) }));
170
- }
171
- return nr;
172
- }),
173
- },
174
- };
175
-
176
- // if swc plugins are provided, inject them into swc-loader options for js/jsx and ts/tsx rules
177
- if (opts.swcPlugins && Array.isArray(opts.swcPlugins) && opts.swcPlugins.length > 0) {
178
- const rules = localConfig.module && localConfig.module.rules;
179
- if (Array.isArray(rules)) {
180
- for (const rule of rules) {
181
- const ruleUse = rule as RuleSetRule;
182
- if (ruleUse.use && Array.isArray(ruleUse.use)) {
183
- for (const entry of ruleUse.use ) {
184
- const useEntry = entry as RuleSetLoaderWithOptions;
185
- if (useEntry && useEntry.loader && typeof useEntry.loader === 'string' && useEntry.loader.includes('swc-loader')) {
186
- const options = ( useEntry.options || {} ) as Record<string, any>;
187
- useEntry.options = options;
188
- useEntry.options.jsc = useEntry.options.jsc || {};
189
- useEntry.options.jsc.experimental = useEntry.options.jsc.experimental || {};
190
- // ensure plugins run before other transforms (important for Relay plugin)
191
- useEntry.options.jsc.experimental.runPluginFirst = true;
192
- // existing plugins may be present under jsc.experimental.plugins; merge them with provided ones
193
- const existingPlugins = Array.isArray(useEntry.options.jsc.experimental.plugins) ? useEntry.options.jsc.experimental.plugins : [];
194
- const incomingPlugins = Array.isArray(opts.swcPlugins) ? opts.swcPlugins : [];
195
- // simple dedupe by plugin name (first element of tuple) to avoid duplicates
196
- const seen = new Set<string>();
197
- const merged: any[] = [];
198
- for (const p of existingPlugins.concat(incomingPlugins)) {
199
- // plugin can be [name, options] or string; normalize
200
- const name = Array.isArray(p) && p.length > 0 ? String(p[0]) : String(p);
201
- if (!seen.has(name)) {
202
- seen.add(name);
203
- merged.push(p);
204
- }
205
- }
206
- useEntry.options.jsc.experimental.plugins = merged;
207
- }
208
- }
209
- }
210
- }
211
- }
212
- }
213
-
214
- // If postcssPlugins are provided, swap the default lightningcss-loader CSS rule
215
- // for postcss-loader with the given plugins.
216
- if (opts.postcssPlugins && opts.postcssPlugins.length > 0) {
217
- const rules: any[] = localConfig.module?.rules ?? [];
218
- for (const rule of rules) {
219
- if (rule?.test instanceof RegExp && rule.test.source === '\\.css$') {
220
- rule.use = [{
221
- loader: 'postcss-loader',
222
- options: { postcssOptions: { plugins: opts.postcssPlugins } },
223
- }];
224
- break;
225
- }
226
- }
227
- }
228
-
229
- if (opts.moduleRules && opts.moduleRules.length > 0) {
230
- localConfig.module.rules.push(...opts.moduleRules);
231
- }
232
-
233
- // slim-react: the SSR-only React-compatible renderer bundled with hadars.
234
- // On server builds we replace the real React with slim-react so that hooks
235
- // get safe SSR stubs, context works, and renderToStream / Suspense are
236
- // natively supported. The client build is untouched and uses real React.
237
- const slimReactIndex = pathMod.resolve(packageDir, 'slim-react', 'index.js');
238
- const slimReactJsx = pathMod.resolve(packageDir, 'slim-react', 'jsx-runtime.js');
239
-
240
- const resolveAliases: Record<string, string> | undefined = isServerBuild ? {
241
- // Route all React imports to slim-react for SSR.
242
- react: slimReactIndex,
243
- 'react/jsx-runtime': slimReactJsx,
244
- 'react/jsx-dev-runtime': slimReactJsx,
245
- // @emotion/* is bundled (not external) so that its `react` imports are
246
- // resolved through the alias above to slim-react. If left external,
247
- // emotion loads real React from node_modules and calls
248
- // ReactSharedInternals.H.useContext which requires React's dispatcher.
249
- } : undefined;
250
-
251
- const externals = isServerBuild ? [
252
- // Node.js built-ins — must not be bundled; resolved by the runtime.
253
- 'node:fs', 'node:path', 'node:os', 'node:stream', 'node:util',
254
- // @emotion/server is only used outside component rendering (CSS extraction)
255
- // and does not call React hooks, so it is safe to leave as external.
256
- '@emotion/server',
257
- ] : undefined;
258
-
259
- // reactMode lets the caller force React's dev/prod runtime independently of
260
- // the webpack build mode. Only applies to the client bundle (SSR uses slim-react).
261
- // 'development' → process.env.NODE_ENV = "development" + JSX dev transform.
262
- const effectiveReactDev = isServerBuild
263
- ? false // slim-react doesn't use NODE_ENV
264
- : opts.reactMode === 'development' ? true
265
- : opts.reactMode === 'production' ? false
266
- : isDev; // default: follow build mode
267
-
268
- if (!isServerBuild && opts.reactMode !== undefined) {
269
- // Override the SWC JSX development flag for all js/ts rules already built
270
- const rules = localConfig.module?.rules ?? [];
271
- for (const rule of rules) {
272
- if (!rule?.use || !Array.isArray(rule.use)) continue;
273
- for (const entry of rule.use) {
274
- if (entry?.loader?.includes('swc-loader')) {
275
- entry.options = entry.options ?? {};
276
- entry.options.jsc = entry.options.jsc ?? {};
277
- entry.options.jsc.transform = entry.options.jsc.transform ?? {};
278
- entry.options.jsc.transform.react = entry.options.jsc.transform.react ?? {};
279
- entry.options.jsc.transform.react.development = effectiveReactDev;
280
- entry.options.jsc.transform.react.refresh = effectiveReactDev && isDev;
281
- }
282
- }
283
- }
284
- }
285
-
286
- const extraPlugins: any[] = [];
287
-
288
- const defineValues: Record<string, string> = { ...(opts.define ?? {}) };
289
- // When reactMode overrides the React runtime we must also set process.env.NODE_ENV
290
- // so React picks its dev/prod bundle, independently of the rspack build mode.
291
- if (!isServerBuild && opts.reactMode !== undefined) {
292
- defineValues['process.env.NODE_ENV'] = JSON.stringify(opts.reactMode);
293
- }
294
- if (Object.keys(defineValues).length > 0) {
295
- const DefinePlugin = (rspack as any).DefinePlugin || (rspack as any).plugins?.DefinePlugin;
296
- if (DefinePlugin) {
297
- extraPlugins.push(new DefinePlugin(defineValues));
298
- }
299
- }
300
-
301
- const resolveConfig: any = {
302
- extensions: ['.tsx', '.ts', '.js', '.jsx'],
303
- alias: resolveAliases,
304
- // for server builds prefer the package "main"/"module" fields and avoid "browser" so we don't pick browser-specific entrypoints
305
- mainFields: isServerBuild ? ['main', 'module'] : ['browser', 'module', 'main'],
306
- // for server builds exclude the "browser" condition so packages with package.json
307
- // "exports" conditions (e.g. @emotion/*) resolve their Node/CJS entry, not the browser build
308
- ...(isServerBuild ? { conditionNames: ['node', 'require', 'default'] } : {}),
309
- };
310
-
311
- // Production client builds get vendor splitting and deterministic module IDs.
312
- // User-supplied optimization is merged on top so it can extend or override defaults.
313
- // Dev and SSR builds skip this — splitChunks slows HMR, SSR uses externals instead.
314
- const optimization: any = (!isServerBuild && !isDev) ? {
315
- moduleIds: 'deterministic',
316
- splitChunks: {
317
- chunks: 'all',
318
- cacheGroups: {
319
- react: {
320
- test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
321
- name: 'vendor-react',
322
- chunks: 'all' as const,
323
- priority: 20,
324
- },
325
- },
326
- },
327
- ...(opts.optimization ?? {}),
328
- } : (opts.optimization ? { ...opts.optimization } : undefined);
329
-
330
- return {
331
- entry,
332
- output: {
333
- ...opts.output,
334
- clean: false,
335
- },
336
- mode: opts.mode,
337
- // Persist transformed modules to disk — subsequent starts only recompile
338
- // changed files, making repeat dev starts significantly faster.
339
- cache: true,
340
- externals,
341
- // externalsPresets.node externalises ALL Node.js built-ins (bare names
342
- // and the node: prefix) for both static and dynamic imports. This
343
- // complements the explicit `externals` array: the preset handles the
344
- // node: URI scheme that rspack cannot resolve as a file, while the
345
- // array keeps '@emotion/server' as an explicit external.
346
- ...(isServerBuild ? { externalsPresets: { node: true } } : {}),
347
- ...(optimization !== undefined ? { optimization } : {}),
348
- plugins: [
349
- !isServerBuild && new rspack.HtmlRspackPlugin({
350
- publicPath: base || '/',
351
- template: opts.htmlTemplate
352
- ? pathMod.resolve(process.cwd(), opts.htmlTemplate)
353
- : clientScriptPath,
354
- scriptLoading: 'module',
355
- filename: 'out.html',
356
- inject: 'head',
357
- minify: opts.mode === 'production',
358
- }),
359
- !isServerBuild && {
360
- apply(compiler: any) {
361
- compiler.hooks.emit.tapAsync('HadarsAsyncModuleScript', (compilation: any, cb: () => void) => {
362
- const asset = compilation.assets['out.html'];
363
- if (asset) {
364
- const html: string = asset.source();
365
- const updated = html.replace(
366
- /(<script\b[^>]*\btype="module"[^>]*)(>)/g,
367
- (match, before: string, end: string) =>
368
- before.includes('async') ? match : `${before} async${end}`,
369
- );
370
- compilation.assets['out.html'] = {
371
- source: () => updated,
372
- size: () => Buffer.byteLength(updated),
373
- };
374
- }
375
- cb();
376
- });
377
- },
378
- },
379
- isDev && !isServerBuild && new ReactRefreshPlugin(),
380
- includeHotPlugin && isDev && !isServerBuild && new rspack.HotModuleReplacementPlugin(),
381
- ...extraPlugins,
382
- ...(opts.plugins ?? []),
383
- ],
384
- ...localConfig,
385
- // Merge base resolve (modules, tsConfig, extensions) with per-build resolve
386
- // (alias, mainFields). The spread order matters: resolveConfig wins for keys
387
- // it defines, localConfig.resolve wins for keys it defines exclusively.
388
- resolve: {
389
- ...localConfig.resolve,
390
- ...resolveConfig,
391
- },
392
- // HMR is not implemented for module chunk format, so disable outputModule
393
- // for client builds. SSR builds still need it for dynamic import() of exports.
394
- experiments: {
395
- ...(localConfig.experiments || {}),
396
- outputModule: isServerBuild,
397
- },
398
- // Prevent rspack from watching its own build output — without this the
399
- // SSR watcher writing .hadars/index.ssr.js triggers the client compiler
400
- // and vice versa, causing an infinite rebuild loop.
401
- watchOptions: {
402
- ignored: ['**/node_modules/**', '**/.hadars/**', '/tmp/**'],
403
- },
404
- };
405
- };
406
-
407
- /**
408
- * Creates a configured rspack compiler for the client bundle without running it.
409
- * Intended for use with RspackDevServer for proper HMR support.
410
- * HotModuleReplacementPlugin is intentionally omitted — RspackDevServer adds it automatically.
411
- */
412
- export const createClientCompiler = (entry: string, opts: EntryOptions) => {
413
- return rspack(buildCompilerConfig(entry, opts, false));
414
- };
415
-
416
- export const compileEntry = async (entry: string, opts: EntryOptions & { watch?: boolean, onChange?: (stats:any)=>void }) => {
417
- const compiler = rspack(buildCompilerConfig(entry, opts, true));
418
-
419
- // If watch mode is requested, start watching and invoke onChange for each rebuild.
420
- // The returned promise resolves once the first build completes so callers can
421
- // await initial build completion before starting their own server.
422
- if (opts.watch) {
423
- await new Promise((resolve, reject) => {
424
- let first = true;
425
- // Pass ignored patterns directly — compiler.watch(watchOptions) replaces
426
- // the config-level watchOptions, so we must repeat them here.
427
- compiler.watch({ ignored: ['**/node_modules/**', '**/.hadars/**', '/tmp/**'] }, (err: any, stats: any) => {
428
- if (err) {
429
- if (first) { first = false; reject(err); }
430
- else { console.error('rspack watch error', err); }
431
- return;
432
- }
433
-
434
- console.log(stats?.toString({ colors: true }));
435
-
436
- if (first) {
437
- first = false;
438
- resolve(stats);
439
- } else {
440
- try {
441
- opts.onChange && opts.onChange(stats);
442
- } catch (e) {
443
- console.error('onChange handler error', e);
444
- }
445
- }
446
- });
447
- });
448
- return;
449
- }
450
-
451
- // non-watch: do a single run and resolve when complete
452
- await new Promise((resolve, reject) => {
453
- compiler.run((err: any, stats: any) => {
454
- if (err) {
455
- reject(err);
456
- return;
457
- }
458
-
459
- console.log(stats?.toString({
460
- colors: true,
461
- preset: 'minimal',
462
- }));
463
-
464
- resolve(stats);
465
- });
466
- });
467
- }
@@ -1,19 +0,0 @@
1
- /** True when running inside Bun. */
2
- export const isBun = typeof (globalThis as any).Bun !== 'undefined';
3
-
4
- /** True when running inside Deno. */
5
- export const isDeno = typeof (globalThis as any).Deno !== 'undefined';
6
-
7
- /** True when running inside Node.js (not Bun, not Deno). */
8
- export const isNode = !isBun && !isDeno;
9
-
10
- /** Returns a human-readable runtime identifier. */
11
- export const getRuntimeName = (): 'bun' | 'deno' | 'node' =>
12
- isBun ? 'bun' : isDeno ? 'deno' : 'node';
13
-
14
- /** Returns a version string for the current runtime, e.g. "Bun 1.1.0". */
15
- export const getRuntimeVersion = (): string => {
16
- if (isBun) return `Bun ${(globalThis as any).Bun.version}`;
17
- if (isDeno) return `Deno ${(globalThis as any).Deno.version.deno}`;
18
- return `Node.js ${process.version}`;
19
- };
@@ -1,155 +0,0 @@
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
- function withRequestLogging(handler: FetchHandler): FetchHandler {
50
- return async (req, ctx) => {
51
- const start = performance.now();
52
- const res = await handler(req, ctx);
53
- const ms = Math.round(performance.now() - start);
54
- const status = res?.status ?? 404;
55
- const path = new URL(req.url).pathname;
56
- console.log(`[hadars] ${req.method} ${path} ${status} ${ms}ms`);
57
- return res;
58
- };
59
- }
60
-
61
-
62
- export async function serve(
63
- port: number,
64
- fetchHandler: FetchHandler,
65
- /** Bun WebSocketHandler — ignored on Deno and Node.js. */
66
- websocket?: unknown,
67
- ): Promise<void> {
68
- fetchHandler = withRequestLogging(fetchHandler);
69
-
70
- // ── Bun ────────────────────────────────────────────────────────────────
71
- if (isBun) {
72
- (globalThis as any).Bun.serve({
73
- port,
74
- websocket,
75
- async fetch(req: Request, server: any) {
76
- const ctx: ServerContext = { upgrade: (r) => server.upgrade(r) };
77
- // Returning undefined from a Bun fetch handler means the
78
- // request was handled as a WebSocket upgrade.
79
- return (await fetchHandler(req, ctx)) ?? undefined;
80
- },
81
- });
82
- return;
83
- }
84
-
85
- // ── Deno ───────────────────────────────────────────────────────────────
86
- // Deno 2.x changed the signature to options-first. Use the single-object
87
- // form { port, handler } which is stable across Deno 1.x and 2.x.
88
- if (isDeno) {
89
- (globalThis as any).Deno.serve({
90
- port,
91
- handler: async (req: Request) => {
92
- const res = await fetchHandler(req, noopCtx);
93
- return res ?? new Response('Not Found', { status: 404 });
94
- },
95
- });
96
- return;
97
- }
98
-
99
- // ── Node.js ────────────────────────────────────────────────────────────
100
- const { createServer } = await import('node:http');
101
-
102
- const server = createServer(async (nodeReq, nodeRes) => {
103
- try {
104
- // Collect body for non-GET/HEAD requests
105
- const chunks: Buffer[] = [];
106
- if (!['GET', 'HEAD'].includes(nodeReq.method ?? 'GET')) {
107
- for await (const chunk of nodeReq) {
108
- chunks.push(chunk as Buffer);
109
- }
110
- }
111
- const body = chunks.length > 0 ? Buffer.concat(chunks) : undefined;
112
-
113
- const url = `http://localhost:${port}${nodeReq.url ?? '/'}`;
114
- const reqInit: RequestInit & { duplex?: string } = {
115
- method: nodeReq.method ?? 'GET',
116
- headers: new Headers(nodeReq.headers as Record<string, string>),
117
- };
118
- if (body) {
119
- reqInit.body = body;
120
- reqInit.duplex = 'half';
121
- }
122
- const req = new Request(url, reqInit);
123
-
124
- const res = await fetchHandler(req, noopCtx);
125
- const response = res ?? new Response('Not Found', { status: 404 });
126
-
127
- const headers: Record<string, string> = {};
128
- response.headers.forEach((v, k) => { headers[k] = v; });
129
- nodeRes.writeHead(response.status, headers);
130
-
131
- if (response.body) {
132
- const reader = response.body.getReader();
133
- while (true) {
134
- const { done, value } = await reader.read();
135
- if (done) break;
136
- await new Promise<void>((resolve, reject) =>
137
- nodeRes.write(value, (err) => (err ? reject(err) : resolve())),
138
- );
139
- }
140
- }
141
- } catch (err) {
142
- console.error('[hadars] request error', err);
143
- if (!nodeRes.headersSent) nodeRes.writeHead(500);
144
- } finally {
145
- nodeRes.end();
146
- }
147
- });
148
-
149
- await new Promise<void>((resolve, reject) => {
150
- server.listen(port, () => resolve());
151
- server.once('error', reject);
152
- });
153
- }
154
-
155
- export { nodeReadableToWebStream };