hadars 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/cli-bun.ts +13 -0
- package/cli-lib.ts +203 -0
- package/cli.ts +13 -0
- package/dist/cli.js +1441 -0
- package/dist/index.cjs +303 -0
- package/dist/index.d.ts +160 -0
- package/dist/index.js +263 -0
- package/dist/loader.cjs +34 -0
- package/dist/ssr-render-worker.js +92 -0
- package/dist/ssr-watch.js +345 -0
- package/dist/template.html +11 -0
- package/dist/utils/clientScript.tsx +58 -0
- package/index.ts +15 -0
- package/package.json +99 -0
- package/src/build.ts +716 -0
- package/src/index.tsx +41 -0
- package/src/ssr-render-worker.ts +138 -0
- package/src/ssr-watch.ts +56 -0
- package/src/types/global.d.ts +5 -0
- package/src/types/ninety.ts +116 -0
- package/src/utils/Head.tsx +357 -0
- package/src/utils/clientScript.tsx +58 -0
- package/src/utils/cookies.ts +16 -0
- package/src/utils/loadModule.ts +4 -0
- package/src/utils/loader.ts +41 -0
- package/src/utils/proxyHandler.tsx +101 -0
- package/src/utils/request.tsx +9 -0
- package/src/utils/response.tsx +198 -0
- package/src/utils/rspack.ts +359 -0
- package/src/utils/runtime.ts +19 -0
- package/src/utils/serve.ts +140 -0
- package/src/utils/staticFile.ts +48 -0
- package/src/utils/template.html +11 -0
- package/src/utils/upgradeRequest.tsx +19 -0
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
|
+
});
|
package/src/ssr-watch.ts
ADDED
|
@@ -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,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
|
+
}
|