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 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
+ })