hadars 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.tsx ADDED
@@ -0,0 +1,41 @@
1
+ export type {
2
+ HadarsOptions,
3
+ HadarsProps,
4
+ HadarsRequest,
5
+ HadarsGetAfterRenderProps,
6
+ HadarsGetFinalProps,
7
+ HadarsGetInitialProps,
8
+ HadarsGetClientProps,
9
+ HadarsEntryModule,
10
+ HadarsApp,
11
+ } from "./types/ninety";
12
+ export { Head as HadarsHead, useServerData } from './utils/Head';
13
+ import { AppProviderSSR, AppProviderCSR } from "./utils/Head";
14
+
15
+ export const HadarsContext = typeof window === 'undefined' ? AppProviderSSR : AppProviderCSR;
16
+
17
+ /**
18
+ * Dynamically loads a module with target-aware behaviour:
19
+ *
20
+ * - **Browser** (after loader transform): becomes `import('./path')`, which
21
+ * rspack splits into a separate chunk for true code splitting.
22
+ * - **SSR** (after loader transform): becomes
23
+ * `Promise.resolve(require('./path'))`, which rspack bundles statically.
24
+ *
25
+ * The hadars rspack loader must be active for the transform to apply.
26
+ * This runtime fallback uses a plain dynamic import (no code splitting).
27
+ *
28
+ * @example
29
+ * // Code-split React component:
30
+ * const MyComp = React.lazy(() => loadModule('./MyComp'));
31
+ *
32
+ * // Dynamic data:
33
+ * const { default: fn } = await loadModule<typeof import('./util')>('./util');
34
+ */
35
+ export function loadModule<T = any>(path: string): Promise<T> {
36
+ // webpackIgnore suppresses rspack's "critical dependency" warning for the
37
+ // variable import. This body is a fallback only — the hadars loader
38
+ // transforms every loadModule('./...') call site at compile time, so this
39
+ // function is never invoked in a bundled build.
40
+ return import(/* webpackIgnore: true */ path) as Promise<T>;
41
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * SSR render worker — runs in a node:worker_threads thread.
3
+ *
4
+ * Handles three message types sent by RenderWorkerPool in build.ts:
5
+ *
6
+ * { type: 'staticMarkup', id, props }
7
+ * → renderToStaticMarkup(_Component, props)
8
+ * → postMessage({ id, type: 'staticMarkup', html, context: props.context })
9
+ *
10
+ * { type: 'renderString', id, appProps, clientProps }
11
+ * → renderToString(ReactPage)
12
+ * → postMessage({ id, html })
13
+ *
14
+ * { type: 'renderStream', id, appProps, clientProps }
15
+ * → renderToReadableStream(ReactPage) — streams chunks back
16
+ * → postMessage({ id, type: 'chunk', chunk }) × N
17
+ * → postMessage({ id, type: 'done' })
18
+ *
19
+ * The SSR bundle path is passed once via workerData at thread creation time so
20
+ * the component is only imported once per worker lifetime.
21
+ */
22
+
23
+ import { workerData, parentPort } from 'node:worker_threads';
24
+ import { createRequire } from 'node:module';
25
+ import pathMod from 'node:path';
26
+ import { pathToFileURL } from 'node:url';
27
+
28
+ const { ssrBundlePath } = workerData as { ssrBundlePath: string };
29
+
30
+ // Lazy-loaded singletons resolved from the *project's* node_modules so the
31
+ // same React instance is shared with the SSR bundle (prevents invalid hook calls).
32
+ let _React: any = null;
33
+ let _renderToStaticMarkup: ((element: any) => string) | null = null;
34
+ let _renderToString: ((element: any) => string) | null = null;
35
+ let _renderToReadableStream: ((element: any, options?: any) => Promise<ReadableStream<Uint8Array>>) | null = null;
36
+ let _Component: any = null;
37
+
38
+ async function init() {
39
+ if (_React && _renderToStaticMarkup && _renderToString && _renderToReadableStream && _Component) return;
40
+
41
+ const req = createRequire(pathMod.resolve(process.cwd(), '__ninety_fake__.js'));
42
+
43
+ if (!_React) {
44
+ const reactPath = pathToFileURL(req.resolve('react')).href;
45
+ const reactMod = await import(reactPath);
46
+ _React = reactMod.default ?? reactMod;
47
+ }
48
+
49
+ if (!_renderToString || !_renderToStaticMarkup) {
50
+ const serverPath = pathToFileURL(req.resolve('react-dom/server')).href;
51
+ const serverMod = await import(serverPath);
52
+ _renderToString = serverMod.renderToString;
53
+ _renderToStaticMarkup = serverMod.renderToStaticMarkup;
54
+ }
55
+
56
+ if (!_renderToReadableStream) {
57
+ const browserPath = pathToFileURL(req.resolve('react-dom/server.browser')).href;
58
+ const browserMod = await import(browserPath);
59
+ _renderToReadableStream = browserMod.renderToReadableStream;
60
+ }
61
+
62
+ if (!_Component) {
63
+ const ssrMod = await import(pathToFileURL(ssrBundlePath).href);
64
+ _Component = ssrMod.default;
65
+ }
66
+ }
67
+
68
+ // Build the full ReactPage element tree — mirrors the shape in response.tsx / build.ts.
69
+ // Uses React.createElement to avoid needing a JSX transform in the worker.
70
+ function buildReactPage(R: any, appProps: Record<string, unknown>, clientProps: Record<string, unknown>) {
71
+ return R.createElement(
72
+ R.Fragment, null,
73
+ R.createElement('div', { id: 'app' },
74
+ R.createElement(_Component, appProps),
75
+ ),
76
+ R.createElement('script', {
77
+ id: 'hadars',
78
+ type: 'application/json',
79
+ dangerouslySetInnerHTML: {
80
+ __html: JSON.stringify({ hadars: { props: clientProps } }),
81
+ },
82
+ }),
83
+ );
84
+ }
85
+
86
+ parentPort!.on('message', async (msg: any) => {
87
+ const { id, type } = msg;
88
+ try {
89
+ await init();
90
+ const R = _React;
91
+
92
+ // Expose the resolved useServerData() cache to the SSR bundle via
93
+ // globalThis.__hadarsUnsuspend — same bridge used on the main thread.
94
+ // Safe in a worker because rendering is sequential (no concurrent requests).
95
+ const unsuspend = (msg.appProps?.context as any)?._unsuspend ?? null;
96
+
97
+ // ── renderStream — streaming response via ReadableStream chunks ───────────
98
+ if (type === 'renderStream') {
99
+ const { appProps, clientProps } = msg as {
100
+ appProps: Record<string, unknown>;
101
+ clientProps: Record<string, unknown>;
102
+ };
103
+ const ReactPage = buildReactPage(R, appProps, clientProps);
104
+ (globalThis as any).__hadarsUnsuspend = unsuspend;
105
+ const stream = await _renderToReadableStream!(ReactPage);
106
+ (globalThis as any).__hadarsUnsuspend = null;
107
+ const reader = stream.getReader();
108
+ while (true) {
109
+ const { done, value } = await reader.read();
110
+ if (done) break;
111
+ // Transfer the underlying ArrayBuffer to avoid a copy across threads.
112
+ parentPort!.postMessage({ id, type: 'chunk', chunk: value }, [value.buffer as ArrayBuffer]);
113
+ }
114
+ parentPort!.postMessage({ id, type: 'done' });
115
+ return;
116
+ }
117
+
118
+ // ── renderString (type === 'renderString' or legacy messages) ────────────
119
+ const { appProps, clientProps } = msg as {
120
+ appProps: Record<string, unknown>;
121
+ clientProps: Record<string, unknown>;
122
+ };
123
+ const ReactPage = buildReactPage(R, appProps, clientProps);
124
+ (globalThis as any).__hadarsUnsuspend = unsuspend;
125
+ const html = _renderToString!(ReactPage);
126
+ (globalThis as any).__hadarsUnsuspend = null;
127
+ parentPort!.postMessage({ id, html });
128
+
129
+ } catch (err: any) {
130
+ (globalThis as any).__hadarsUnsuspend = null;
131
+ const errMsg = err?.message ?? String(err);
132
+ if (type === 'renderStream') {
133
+ parentPort!.postMessage({ id, type: 'error', error: errMsg });
134
+ } else {
135
+ parentPort!.postMessage({ id, error: errMsg });
136
+ }
137
+ }
138
+ });
@@ -0,0 +1,56 @@
1
+ import pathMod from 'node:path';
2
+ import { compileEntry } from './utils/rspack';
3
+
4
+ // Simple worker to run an SSR watch in a separate process to avoid multiple
5
+ // rspack instances in the same process.
6
+
7
+ const args = process.argv.slice(2);
8
+ const argv: Record<string, string> = {};
9
+ for (const a of args) {
10
+ if (!a.startsWith('--')) continue;
11
+ const idx = a.indexOf('=');
12
+ if (idx === -1) continue;
13
+ const key = a.slice(2, idx);
14
+ const val = a.slice(idx + 1);
15
+ argv[key] = val;
16
+ }
17
+
18
+ const entry = argv['entry'];
19
+ const outDir = argv['outDir'] || '.hadars';
20
+ const outFile = argv['outFile'] || 'index.ssr.js';
21
+ const base = argv['base'] || '';
22
+
23
+ if (!entry) {
24
+ console.error('ssr-watch: missing --entry argument');
25
+ process.exit(1);
26
+ }
27
+
28
+ (async () => {
29
+ try {
30
+ console.log('ssr-watch: starting SSR watcher for', entry);
31
+ await compileEntry(entry, {
32
+ target: 'node',
33
+ output: {
34
+ iife: false,
35
+ filename: outFile,
36
+ path: pathMod.resolve(process.cwd(), outDir),
37
+ publicPath: '',
38
+ library: { type: 'module' }
39
+ },
40
+ base,
41
+ mode: 'development',
42
+ watch: true,
43
+ onChange: () => {
44
+ console.log('ssr-watch: SSR rebuilt');
45
+ }
46
+ } as any);
47
+ // compileEntry with watch resolves after the initial build completes. Keep the
48
+ // process alive so the watcher remains active, but first emit a clear marker
49
+ // so the parent process can detect initial build completion.
50
+ console.log('ssr-watch: initial-build-complete');
51
+ await new Promise(() => { /* never resolve - keep worker alive */ });
52
+ } catch (err) {
53
+ console.error('ssr-watch: error', err);
54
+ process.exit(1);
55
+ }
56
+ })();
@@ -0,0 +1,5 @@
1
+ declare module '$_MOD_PATH$' {
2
+ const value: any;
3
+ export default value;
4
+ }
5
+
@@ -0,0 +1,116 @@
1
+ import type { LinkHTMLAttributes, MetaHTMLAttributes, ScriptHTMLAttributes, StyleHTMLAttributes } from "react";
2
+
3
+ export type HadarsGetInitialProps<T extends {}> = (req: HadarsRequest) => Promise<T> | T;
4
+ export type HadarsGetClientProps<T extends {}> = (props: T) => Promise<T> | T;
5
+ export type HadarsGetAfterRenderProps<T extends {}> = (props: HadarsProps<T>, html: string) => Promise<HadarsProps<T>> | HadarsProps<T>;
6
+ export type HadarsGetFinalProps<T extends {}> = (props: HadarsProps<T>) => Promise<T> | T;
7
+ export type HadarsApp<T extends {}> = React.FC<HadarsProps<T>>;
8
+
9
+ export type HadarsEntryModule<T extends {}> = {
10
+ default: HadarsApp<T>;
11
+ getInitProps?: HadarsGetInitialProps<T>;
12
+ getAfterRenderProps?: HadarsGetAfterRenderProps<T>;
13
+ getFinalProps?: HadarsGetFinalProps<T>;
14
+ getClientProps?: HadarsGetClientProps<T>;
15
+ }
16
+
17
+ export interface AppHead {
18
+ title: string;
19
+ status: number;
20
+ meta: Record<string, MetaProps>;
21
+ link: Record<string, LinkProps>;
22
+ style: Record<string, StyleProps>;
23
+ script: Record<string, ScriptProps>;
24
+ }
25
+
26
+ export type UnsuspendEntry =
27
+ | { status: 'pending'; promise: Promise<unknown> }
28
+ | { status: 'fulfilled'; value: unknown }
29
+ | { status: 'suspense-resolved' }
30
+ | { status: 'rejected'; reason: unknown };
31
+
32
+ /** @internal Populated by the framework's render loop — use useServerData() instead. */
33
+ export interface AppUnsuspend {
34
+ cache: Map<string, UnsuspendEntry>;
35
+ hasPending: boolean;
36
+ }
37
+
38
+ export interface AppContext {
39
+ path?: string;
40
+ head: AppHead;
41
+ /** @internal Framework use only — use the useServerData() hook instead. */
42
+ _unsuspend?: AppUnsuspend;
43
+ }
44
+
45
+ export type HadarsEntryBase = {
46
+ location: string;
47
+ context: AppContext;
48
+ }
49
+
50
+ export type HadarsProps<T extends {}> = T & HadarsEntryBase;
51
+
52
+ export type MetaProps = MetaHTMLAttributes<HTMLMetaElement>;
53
+ export type LinkProps = LinkHTMLAttributes<HTMLLinkElement>;
54
+ export type StyleProps = StyleHTMLAttributes<HTMLStyleElement>;
55
+ export type ScriptProps = ScriptHTMLAttributes<HTMLScriptElement>;
56
+
57
+ export interface HadarsOptions {
58
+ port?: number;
59
+ entry: string;
60
+ baseURL?: string;
61
+ // Optional SWC plugins to be applied to the swc-loader during compilation
62
+ swcPlugins?: SwcPluginList;
63
+ proxy?: Record<string, string> | ((req: HadarsRequest) => Promise<Response | null> | Response | null);
64
+ proxyCORS?: boolean;
65
+ define?: Record<string, string>;
66
+ /**
67
+ * Bun WebSocket handler passed directly to `Bun.serve()`.
68
+ * Ignored on Node.js and Deno — use `fetch` + a third-party WS library there.
69
+ * Pass a `Bun.WebSocketHandler` instance here when running on Bun.
70
+ */
71
+ websocket?: unknown;
72
+ fetch?: (req: HadarsRequest) => Promise<Response | undefined> | Response | undefined;
73
+ wsPath?: string;
74
+ // Port for the rspack HMR dev server. Defaults to port + 1.
75
+ hmrPort?: number;
76
+ /**
77
+ * Parallelism level for `run()` mode (production server). Defaults to 1.
78
+ * Has no effect in `dev()` mode.
79
+ *
80
+ * **Node.js** — forks this many worker processes via `node:cluster`, each
81
+ * binding to the same port via OS round-robin. Set to `os.cpus().length`
82
+ * to saturate all CPU cores.
83
+ *
84
+ * **Bun / Deno** — creates a `node:worker_threads` render pool of this size.
85
+ * Each thread handles the synchronous `renderToString` step, freeing the
86
+ * main event loop for I/O. Only applies when `streaming` is `false` (the default).
87
+ */
88
+ workers?: number;
89
+ /**
90
+ * Whether to use streaming SSR (`renderToReadableStream`) for rendering React
91
+ * components. Defaults to `false`. Set to `true` to use streaming via
92
+ * `renderToReadableStream` instead of the synchronous `renderToString`.
93
+ */
94
+ streaming?: boolean;
95
+ }
96
+
97
+
98
+ // SWC plugin typing — a pragmatic, ergonomic union that matches common usages:
99
+ // - a plugin package name (string)
100
+ // - a tuple [pluginName, options]
101
+ // - an object with a path and optional options
102
+ // - a direct function (for programmatic plugin instances)
103
+ export type SwcPluginItem =
104
+ | string
105
+ | [string, Record<string, unknown>]
106
+ | { path: string; options?: Record<string, unknown> }
107
+ | ((...args: any[]) => any);
108
+
109
+ export type SwcPluginList = SwcPluginItem[];
110
+
111
+ export interface HadarsRequest extends Request {
112
+ pathname: string;
113
+ search: string;
114
+ location: string;
115
+ cookies: Record<string, string>;
116
+ }