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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ninety contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# hadars
|
|
2
|
+
|
|
3
|
+
A minimal server-side rendering framework for React built on [rspack](https://rspack.dev). Runs on Bun, Node.js, and Deno.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install hadars
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
**hadars.config.ts**
|
|
14
|
+
```ts
|
|
15
|
+
import os from 'os';
|
|
16
|
+
import type { HadarsOptions } from 'hadars';
|
|
17
|
+
|
|
18
|
+
const config: HadarsOptions = {
|
|
19
|
+
entry: 'src/App.tsx',
|
|
20
|
+
port: 3000,
|
|
21
|
+
workers: os.cpus().length, // multi-core production server (Node.js)
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default config;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**src/App.tsx**
|
|
28
|
+
```tsx
|
|
29
|
+
import React from 'react';
|
|
30
|
+
import { HadarsContext, HadarsHead, type HadarsApp, type HadarsRequest } from 'hadars';
|
|
31
|
+
|
|
32
|
+
interface Props { user: { name: string } }
|
|
33
|
+
|
|
34
|
+
const App: HadarsApp<Props> = ({ user, context }) => (
|
|
35
|
+
<HadarsContext context={context}>
|
|
36
|
+
<HadarsHead status={200}>
|
|
37
|
+
<title>Hello {user.name}</title>
|
|
38
|
+
</HadarsHead>
|
|
39
|
+
<h1>Hello, {user.name}!</h1>
|
|
40
|
+
</HadarsContext>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export const getInitProps = async (req: HadarsRequest): Promise<Props> => ({
|
|
44
|
+
user: await db.getUser(req.cookies.session),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export default App;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## CLI
|
|
51
|
+
|
|
52
|
+
After installing hadars the `hadars` (Node.js) and `hadars-bun` (Bun) binaries are available:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Development server with React Fast Refresh HMR
|
|
56
|
+
hadars dev
|
|
57
|
+
hadars-bun dev # Bun — runs TypeScript directly, no build step
|
|
58
|
+
|
|
59
|
+
# Production build (client + SSR bundles compiled in parallel)
|
|
60
|
+
hadars build
|
|
61
|
+
|
|
62
|
+
# Serve the production build
|
|
63
|
+
hadars run # multi-core when workers > 1
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Features
|
|
67
|
+
|
|
68
|
+
- **React Fast Refresh** — full HMR via rspack-dev-server, module-level patches
|
|
69
|
+
- **True SSR** — components render on the server with your data, then hydrate on the client
|
|
70
|
+
- **Code splitting** — `loadModule('./Comp')` splits on the browser, bundles statically on the server
|
|
71
|
+
- **Head management** — `HadarsHead` controls `<title>`, `<meta>`, `<link>` on server and client
|
|
72
|
+
- **Cross-runtime** — Bun, Node.js, Deno; uses the standard Fetch API throughout
|
|
73
|
+
- **Multi-core** — `workers: os.cpus().length` forks a process per CPU core via `node:cluster`
|
|
74
|
+
- **TypeScript-first** — full types for props, lifecycle hooks, config, and the request object
|
|
75
|
+
|
|
76
|
+
## Data lifecycle hooks
|
|
77
|
+
|
|
78
|
+
| Hook | Runs on | Purpose |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `getInitProps` | server | Fetch server-side data from the `HadarsRequest` |
|
|
81
|
+
| `getAfterRenderProps` | server | Inspect the rendered HTML (e.g. extract critical CSS) |
|
|
82
|
+
| `getFinalProps` | server | Strip server-only fields before props are serialised to the client |
|
|
83
|
+
| `getClientProps` | client | Enrich props with browser-only data (localStorage, device APIs) |
|
|
84
|
+
|
|
85
|
+
## HadarsOptions
|
|
86
|
+
|
|
87
|
+
| Option | Type | Default | Description |
|
|
88
|
+
|---|---|---|---|
|
|
89
|
+
| `entry` | `string` | — | Path to your page component **(required)** |
|
|
90
|
+
| `port` | `number` | `9090` | HTTP port |
|
|
91
|
+
| `hmrPort` | `number` | `port + 1` | rspack HMR dev server port |
|
|
92
|
+
| `baseURL` | `string` | `""` | Public base path, e.g. `"/app"` |
|
|
93
|
+
| `workers` | `number` | `1` | Worker processes in `run()` mode (Node.js only) |
|
|
94
|
+
| `proxy` | `Record / fn` | — | Path-prefix proxy rules or a custom async function |
|
|
95
|
+
| `proxyCORS` | `boolean` | — | Inject CORS headers on proxied responses |
|
|
96
|
+
| `define` | `Record` | — | Compile-time constants for rspack's DefinePlugin |
|
|
97
|
+
| `swcPlugins` | `array` | — | Extra SWC plugins (e.g. Relay compiler) |
|
|
98
|
+
| `fetch` | `function` | — | Custom fetch handler; return a `Response` to short-circuit SSR |
|
|
99
|
+
| `websocket` | `object` | — | WebSocket handler (Bun only) |
|
|
100
|
+
| `wsPath` | `string` | `"/ws"` | Path that triggers WebSocket upgrade |
|
|
101
|
+
| `streaming` | `boolean` | `false` | Set to `true` to use `renderToReadableStream` (streaming SSR) instead of the default `renderToString` |
|
|
102
|
+
|
|
103
|
+
## Local build
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm install
|
|
107
|
+
npm run build:all
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Publishing
|
|
111
|
+
|
|
112
|
+
1. Update `version`, `repository`, `license` in `package.json`
|
|
113
|
+
2. `npm login`
|
|
114
|
+
3. `npm publish`
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
package/cli-bun.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { runCli } from './cli-lib'
|
|
3
|
+
|
|
4
|
+
runCli(process.argv).catch((err) => {
|
|
5
|
+
console.error(err)
|
|
6
|
+
// When Bun runs a script, allow non-zero exit codes to propagate
|
|
7
|
+
try {
|
|
8
|
+
process.exit(1)
|
|
9
|
+
} catch (_) {
|
|
10
|
+
// Some Bun environments may not allow process.exit; just rethrow
|
|
11
|
+
throw err
|
|
12
|
+
}
|
|
13
|
+
})
|
package/cli-lib.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { resolve, join } from 'node:path'
|
|
4
|
+
import * as Hadars from './src/build'
|
|
5
|
+
import type { HadarsOptions } from './src/types/ninety'
|
|
6
|
+
|
|
7
|
+
const SUPPORTED = ['hadars.config.js', 'hadars.config.mjs', 'hadars.config.cjs', 'hadars.config.ts']
|
|
8
|
+
|
|
9
|
+
function findConfig(cwd: string): string | null {
|
|
10
|
+
for (const name of SUPPORTED) {
|
|
11
|
+
const p = resolve(cwd, name)
|
|
12
|
+
if (existsSync(p)) return p
|
|
13
|
+
}
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function dev(config: HadarsOptions) {
|
|
18
|
+
await Hadars.dev({
|
|
19
|
+
...config,
|
|
20
|
+
baseURL: '',
|
|
21
|
+
mode: 'development',
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function build(config: HadarsOptions) {
|
|
26
|
+
await Hadars.build({
|
|
27
|
+
...config,
|
|
28
|
+
mode: 'production',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function run(config: HadarsOptions) {
|
|
33
|
+
await Hadars.run({
|
|
34
|
+
...config,
|
|
35
|
+
mode: 'production',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function loadConfig(configPath: string): Promise<HadarsOptions> {
|
|
40
|
+
const url = `file://${configPath}`
|
|
41
|
+
const mod = await import(url)
|
|
42
|
+
return (mod && (mod.default ?? mod)) as HadarsOptions
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── hadars new ────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
const TEMPLATES: Record<string, (name: string) => string> = {
|
|
48
|
+
'package.json': (name: string) => JSON.stringify({
|
|
49
|
+
name,
|
|
50
|
+
version: '0.1.0',
|
|
51
|
+
type: 'module',
|
|
52
|
+
private: true,
|
|
53
|
+
scripts: {
|
|
54
|
+
dev: 'hadars dev',
|
|
55
|
+
build: 'hadars build',
|
|
56
|
+
start: 'hadars run',
|
|
57
|
+
},
|
|
58
|
+
dependencies: {
|
|
59
|
+
'hadars': 'latest',
|
|
60
|
+
react: '^19.0.0',
|
|
61
|
+
'react-dom':'^19.0.0',
|
|
62
|
+
},
|
|
63
|
+
}, null, 2) + '\n',
|
|
64
|
+
|
|
65
|
+
'hadars.config.ts': () =>
|
|
66
|
+
`import type { HadarsOptions } from 'hadars';
|
|
67
|
+
|
|
68
|
+
const config: HadarsOptions = {
|
|
69
|
+
entry: 'src/App.tsx',
|
|
70
|
+
port: 3000,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default config;
|
|
74
|
+
`,
|
|
75
|
+
|
|
76
|
+
'tsconfig.json': () => JSON.stringify({
|
|
77
|
+
compilerOptions: {
|
|
78
|
+
lib: ['ESNext', 'DOM'],
|
|
79
|
+
target: 'ESNext',
|
|
80
|
+
module: 'Preserve',
|
|
81
|
+
moduleDetection: 'force',
|
|
82
|
+
jsx: 'react-jsx',
|
|
83
|
+
moduleResolution: 'bundler',
|
|
84
|
+
allowImportingTsExtensions: true,
|
|
85
|
+
verbatimModuleSyntax: true,
|
|
86
|
+
noEmit: true,
|
|
87
|
+
strict: true,
|
|
88
|
+
skipLibCheck: true,
|
|
89
|
+
},
|
|
90
|
+
}, null, 2) + '\n',
|
|
91
|
+
|
|
92
|
+
'.gitignore': () =>
|
|
93
|
+
`node_modules/
|
|
94
|
+
.hadars/
|
|
95
|
+
dist/
|
|
96
|
+
`,
|
|
97
|
+
|
|
98
|
+
'src/App.tsx': () =>
|
|
99
|
+
`import React from 'react';
|
|
100
|
+
import { HadarsContext, HadarsHead, type HadarsApp } from 'hadars';
|
|
101
|
+
|
|
102
|
+
const App: HadarsApp<{}> = ({ context }) => (
|
|
103
|
+
<HadarsContext context={context}>
|
|
104
|
+
<HadarsHead status={200}>
|
|
105
|
+
<title>My App</title>
|
|
106
|
+
</HadarsHead>
|
|
107
|
+
<main>
|
|
108
|
+
<h1>Hello from hadars!</h1>
|
|
109
|
+
<p>Edit <code>src/App.tsx</code> to get started.</p>
|
|
110
|
+
</main>
|
|
111
|
+
</HadarsContext>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
export default App;
|
|
115
|
+
`,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function createProject(name: string, cwd: string): Promise<void> {
|
|
119
|
+
const dir = resolve(cwd, name)
|
|
120
|
+
|
|
121
|
+
if (existsSync(dir)) {
|
|
122
|
+
console.error(`Directory already exists: ${dir}`)
|
|
123
|
+
process.exit(1)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log(`Creating hadars project in ${dir}`)
|
|
127
|
+
|
|
128
|
+
await mkdir(join(dir, 'src'), { recursive: true })
|
|
129
|
+
|
|
130
|
+
for (const [file, template] of Object.entries(TEMPLATES)) {
|
|
131
|
+
const content = template(name)
|
|
132
|
+
await writeFile(join(dir, file), content, 'utf-8')
|
|
133
|
+
console.log(` created ${file}`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log(`
|
|
137
|
+
Done! Next steps:
|
|
138
|
+
|
|
139
|
+
cd ${name}
|
|
140
|
+
npm install # or: bun install / pnpm install
|
|
141
|
+
npm run dev # or: bun run dev
|
|
142
|
+
`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── CLI entry ─────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
function usage(): void {
|
|
148
|
+
console.log('Usage: hadars <new <name> | dev | build | run>')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function runCli(argv: string[], cwd = process.cwd()): Promise<void> {
|
|
152
|
+
const cmd = argv[2]
|
|
153
|
+
|
|
154
|
+
if (cmd === 'new') {
|
|
155
|
+
const name = argv[3]
|
|
156
|
+
if (!name) {
|
|
157
|
+
console.error('Usage: hadars new <project-name>')
|
|
158
|
+
process.exit(1)
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
await createProject(name, cwd)
|
|
162
|
+
} catch (err: any) {
|
|
163
|
+
console.error('Failed to create project:', err?.message ?? err)
|
|
164
|
+
process.exit(2)
|
|
165
|
+
}
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!cmd || !['dev', 'build', 'run'].includes(cmd)) {
|
|
170
|
+
usage()
|
|
171
|
+
process.exit(1)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const configPath = findConfig(cwd)
|
|
175
|
+
if (!configPath) {
|
|
176
|
+
console.log(`No hadars.config.* found in ${cwd}`)
|
|
177
|
+
console.log('Proceeding with default behavior (no config)')
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const cfg = await loadConfig(configPath)
|
|
183
|
+
console.log(`Loaded config from ${configPath}`)
|
|
184
|
+
switch (cmd) {
|
|
185
|
+
case 'dev':
|
|
186
|
+
console.log('Starting development server...')
|
|
187
|
+
await dev(cfg);
|
|
188
|
+
break;
|
|
189
|
+
case 'build':
|
|
190
|
+
console.log('Building project...')
|
|
191
|
+
await build(cfg);
|
|
192
|
+
console.log('Build complete')
|
|
193
|
+
process.exit(0)
|
|
194
|
+
case 'run':
|
|
195
|
+
console.log('Running project...')
|
|
196
|
+
await run(cfg);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
} catch (err: any) {
|
|
200
|
+
console.error('Failed to load config:', err?.message ?? err)
|
|
201
|
+
process.exit(2)
|
|
202
|
+
}
|
|
203
|
+
}
|
package/cli.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runCli } from './cli-lib'
|
|
3
|
+
|
|
4
|
+
runCli(process.argv).catch((err) => {
|
|
5
|
+
console.error(err)
|
|
6
|
+
// When Bun runs a script, allow non-zero exit codes to propagate
|
|
7
|
+
try {
|
|
8
|
+
process.exit(1)
|
|
9
|
+
} catch (_) {
|
|
10
|
+
// Some Bun environments may not allow process.exit; just rethrow
|
|
11
|
+
throw err
|
|
12
|
+
}
|
|
13
|
+
})
|