hadars 0.1.20 → 0.1.21
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/README.md +107 -28
- package/cli-lib.ts +179 -11
- package/dist/cli.js +188 -22
- package/dist/index.d.ts +16 -0
- package/dist/ssr-watch.js +5 -9
- package/package.json +1 -2
- package/src/build.ts +4 -0
- package/src/ssr-watch.ts +2 -0
- package/src/types/hadars.ts +16 -0
- package/src/utils/rspack.ts +6 -10
package/README.md
CHANGED
|
@@ -2,23 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
A minimal server-side rendering framework for React built on [rspack](https://rspack.dev). Runs on Bun, Node.js, and Deno.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Why hadars?
|
|
6
|
+
|
|
7
|
+
hadars is an alternative to Next.js for apps that just need SSR.
|
|
8
|
+
|
|
9
|
+
**Getting out is painful.** Next.js has its own router, image component, font loader, `<Link>`, and middleware API. These aren't just conveniences - they're load-bearing parts of your app. By the time you want to leave, you're not swapping a dependency, you're doing a rewrite.
|
|
10
|
+
|
|
11
|
+
**Server Components solved a problem I don't have.** The mental model is interesting, but the split between server-only and client-only trees, plus the serialisation boundary between them, adds real complexity for most apps. `getInitProps` + `useServerData` gets you server-rendered HTML with client hydration without any of that.
|
|
12
|
+
|
|
13
|
+
**A smaller attack surface is better.** Any framework that intercepts every request adds risk. Less code handling that path is a reasonable starting point - whether that translates to meaningful security differences is something only time and scrutiny will tell.
|
|
14
|
+
|
|
15
|
+
**Less overhead.** hadars skips RSC infrastructure, a built-in router, and edge runtime polyfills. It uses its own SSR renderer ([slim-react](#slim-react)) instead of react-dom/server. Whether that matters for your use case depends on your load, but the baseline is lighter.
|
|
16
|
+
|
|
17
|
+
Bring your own router (or none), keep your components as plain React, and get SSR, HMR, and a production build from a single config file.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
Scaffold a new project in seconds:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx hadars new my-app
|
|
25
|
+
cd my-app
|
|
26
|
+
npm install
|
|
27
|
+
npm run dev
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or install manually:
|
|
6
31
|
|
|
7
32
|
```bash
|
|
8
33
|
npm install hadars
|
|
9
34
|
```
|
|
10
35
|
|
|
11
|
-
##
|
|
36
|
+
## Example
|
|
12
37
|
|
|
13
38
|
**hadars.config.ts**
|
|
14
39
|
```ts
|
|
15
|
-
import os from 'os';
|
|
16
40
|
import type { HadarsOptions } from 'hadars';
|
|
17
41
|
|
|
18
42
|
const config: HadarsOptions = {
|
|
19
43
|
entry: 'src/App.tsx',
|
|
20
44
|
port: 3000,
|
|
21
|
-
workers: os.cpus().length, // multi-core production server (Node.js)
|
|
22
45
|
};
|
|
23
46
|
|
|
24
47
|
export default config;
|
|
@@ -49,9 +72,10 @@ export default App;
|
|
|
49
72
|
|
|
50
73
|
## CLI
|
|
51
74
|
|
|
52
|
-
After installing hadars the `hadars` binary is available. It works on Node.js, Bun, and Deno — the runtime is auto-detected:
|
|
53
|
-
|
|
54
75
|
```bash
|
|
76
|
+
# Scaffold a new project
|
|
77
|
+
hadars new <project-name>
|
|
78
|
+
|
|
55
79
|
# Development server with React Fast Refresh HMR
|
|
56
80
|
hadars dev
|
|
57
81
|
|
|
@@ -59,19 +83,19 @@ hadars dev
|
|
|
59
83
|
hadars build
|
|
60
84
|
|
|
61
85
|
# Serve the production build
|
|
62
|
-
hadars run
|
|
86
|
+
hadars run
|
|
63
87
|
```
|
|
64
88
|
|
|
65
89
|
## Features
|
|
66
90
|
|
|
67
|
-
- **React Fast Refresh**
|
|
68
|
-
- **True SSR**
|
|
69
|
-
- **Shell streaming**
|
|
70
|
-
- **Code splitting**
|
|
71
|
-
- **Head management**
|
|
72
|
-
- **Cross-runtime**
|
|
73
|
-
- **Multi-core**
|
|
74
|
-
- **TypeScript-first**
|
|
91
|
+
- **React Fast Refresh** - full HMR via rspack-dev-server, module-level patches
|
|
92
|
+
- **True SSR** - components render on the server with your data, then hydrate on the client
|
|
93
|
+
- **Shell streaming** - HTML shell is flushed immediately so browsers can start loading assets before the body arrives
|
|
94
|
+
- **Code splitting** - `loadModule('./Comp')` splits on the browser, bundles statically on the server
|
|
95
|
+
- **Head management** - `HadarsHead` controls `<title>`, `<meta>`, `<link>` on server and client
|
|
96
|
+
- **Cross-runtime** - Bun, Node.js, Deno; uses the standard Fetch API throughout
|
|
97
|
+
- **Multi-core** - `workers: os.cpus().length` forks a process per CPU core via `node:cluster`
|
|
98
|
+
- **TypeScript-first** - full types for props, lifecycle hooks, config, and the request object
|
|
75
99
|
|
|
76
100
|
## useServerData
|
|
77
101
|
|
|
@@ -87,10 +111,10 @@ const UserCard = ({ userId }: { userId: string }) => {
|
|
|
87
111
|
};
|
|
88
112
|
```
|
|
89
113
|
|
|
90
|
-
- **`key`**
|
|
91
|
-
- **Server**
|
|
92
|
-
- **Client**
|
|
93
|
-
- **Suspense libraries**
|
|
114
|
+
- **`key`** - string or string array; must be stable and unique within the page
|
|
115
|
+
- **Server** - calls `fn()`, awaits the result across render iterations, returns `undefined` until resolved
|
|
116
|
+
- **Client** - reads the pre-resolved value from the hydration cache serialised by the server; `fn()` is never called in the browser
|
|
117
|
+
- **Suspense libraries** - also works when `fn()` throws a thenable (e.g. Relay `useLazyLoadQuery` with `suspense: true`); the thrown promise is awaited and the next render re-calls `fn()` synchronously
|
|
94
118
|
|
|
95
119
|
## Data lifecycle hooks
|
|
96
120
|
|
|
@@ -105,19 +129,74 @@ const UserCard = ({ userId }: { userId: string }) => {
|
|
|
105
129
|
|
|
106
130
|
| Option | Type | Default | Description |
|
|
107
131
|
|---|---|---|---|
|
|
108
|
-
| `entry` | `string` |
|
|
132
|
+
| `entry` | `string` | - | Path to your page component **(required)** |
|
|
109
133
|
| `port` | `number` | `9090` | HTTP port |
|
|
110
134
|
| `hmrPort` | `number` | `port + 1` | rspack HMR dev server port |
|
|
111
135
|
| `baseURL` | `string` | `""` | Public base path, e.g. `"/app"` |
|
|
112
|
-
| `workers` | `number` | `1` | Worker processes in `run()` mode
|
|
113
|
-
| `proxy` | `Record / fn` |
|
|
114
|
-
| `proxyCORS` | `boolean` |
|
|
115
|
-
| `define` | `Record` |
|
|
116
|
-
| `swcPlugins` | `array` |
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
136
|
+
| `workers` | `number` | `1` | Worker processes / threads in `run()` mode |
|
|
137
|
+
| `proxy` | `Record / fn` | - | Path-prefix proxy rules or a custom async function |
|
|
138
|
+
| `proxyCORS` | `boolean` | - | Inject CORS headers on proxied responses |
|
|
139
|
+
| `define` | `Record` | - | Compile-time constants for rspack's DefinePlugin |
|
|
140
|
+
| `swcPlugins` | `array` | - | Extra SWC plugins (e.g. Relay compiler) |
|
|
141
|
+
| `moduleRules` | `array` | - | Extra rspack module rules appended to the built-in set (client + SSR) |
|
|
142
|
+
| `fetch` | `function` | - | Custom fetch handler; return a `Response` to short-circuit SSR |
|
|
143
|
+
| `websocket` | `object` | - | WebSocket handler (Bun only) |
|
|
119
144
|
| `wsPath` | `string` | `"/ws"` | Path that triggers WebSocket upgrade |
|
|
120
|
-
| `
|
|
145
|
+
| `htmlTemplate` | `string` | - | Path to a custom HTML template with `HADARS_HEAD` / `HADARS_BODY` markers |
|
|
146
|
+
| `optimization` | `object` | - | Override rspack `optimization` for production client builds |
|
|
147
|
+
| `cache` | `function` | - | SSR response cache for `run()` mode; return `{ key, ttl? }` to cache a request |
|
|
148
|
+
|
|
149
|
+
### moduleRules example
|
|
150
|
+
|
|
151
|
+
Add support for any loader not included by default:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import type { HadarsOptions } from 'hadars';
|
|
155
|
+
|
|
156
|
+
const config: HadarsOptions = {
|
|
157
|
+
entry: 'src/App.tsx',
|
|
158
|
+
moduleRules: [
|
|
159
|
+
{
|
|
160
|
+
test: /\.mdx?$/,
|
|
161
|
+
use: [{ loader: '@mdx-js/loader' }],
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export default config;
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### SSR cache example
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import type { HadarsOptions } from 'hadars';
|
|
173
|
+
|
|
174
|
+
const config: HadarsOptions = {
|
|
175
|
+
entry: 'src/App.tsx',
|
|
176
|
+
// Cache every page by pathname for 60 seconds, skip authenticated requests
|
|
177
|
+
cache: (req) => req.cookies.session ? null : { key: req.pathname, ttl: 60_000 },
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export default config;
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## slim-react
|
|
184
|
+
|
|
185
|
+
hadars ships its own lightweight React-compatible SSR renderer called **slim-react** (`src/slim-react/`). It replaces `react-dom/server` entirely on the server side - no `renderToStaticMarkup`, no `renderToPipeableStream`, no react-dom dependency at all.
|
|
186
|
+
|
|
187
|
+
For server builds, rspack aliases `react` and `react/jsx-runtime` to slim-react, so your components and any libraries they import render through it automatically without any code changes.
|
|
188
|
+
|
|
189
|
+
**What it does:**
|
|
190
|
+
|
|
191
|
+
- Renders the full component tree to an HTML string with native `async/await` support - async components and hooks that return Promises are awaited directly without streaming workarounds
|
|
192
|
+
- Implements the React Suspense protocol: when a component throws a Promise (e.g. from `useServerData` or a Suspense-enabled data library), slim-react awaits it and retries the tree automatically
|
|
193
|
+
- Emits React-compatible hydration markers - `<!--$-->…<!--/$-->` for resolved Suspense boundaries, `<!-- -->` separators between adjacent text nodes - so `hydrateRoot` on the client works without mismatches
|
|
194
|
+
- Supports `React.memo`, `React.forwardRef`, `React.lazy`, `Context.Provider`, `Context.Consumer`, and the React 18/19 element wire formats
|
|
195
|
+
- Covers the full hook surface needed for SSR: `useState`, `useReducer`, `useContext`, `useRef`, `useMemo`, `useCallback`, `useId`, `useSyncExternalStore`, `use`, and more - all as lightweight SSR stubs
|
|
196
|
+
|
|
197
|
+
**Why not react-dom/server?**
|
|
198
|
+
|
|
199
|
+
`react-dom/server` cannot `await` arbitrary Promises thrown during render - it only handles Suspense via streaming and requires components to use `React.lazy` or Relay-style Suspense resources. slim-react's retry loop makes `useServerData` (and any hook that throws a Promise) work without wrapping every async component in a `<Suspense>` boundary.
|
|
121
200
|
|
|
122
201
|
## License
|
|
123
202
|
|
package/cli-lib.ts
CHANGED
|
@@ -99,17 +99,185 @@ dist/
|
|
|
99
99
|
`import React from 'react';
|
|
100
100
|
import { HadarsContext, HadarsHead, type HadarsApp } from 'hadars';
|
|
101
101
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
102
|
+
const css = \`
|
|
103
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
104
|
+
|
|
105
|
+
body {
|
|
106
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
107
|
+
background: #0f0f13;
|
|
108
|
+
color: #e2e8f0;
|
|
109
|
+
min-height: 100vh;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.nav {
|
|
113
|
+
display: flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: space-between;
|
|
116
|
+
padding: 1rem 2rem;
|
|
117
|
+
border-bottom: 1px solid #1e1e2e;
|
|
118
|
+
}
|
|
119
|
+
.nav-brand { font-weight: 700; font-size: 1.1rem; color: #a78bfa; letter-spacing: -0.02em; }
|
|
120
|
+
.nav-links { display: flex; gap: 1.5rem; }
|
|
121
|
+
.nav-links a { color: #94a3b8; text-decoration: none; font-size: 0.9rem; }
|
|
122
|
+
.nav-links a:hover { color: #e2e8f0; }
|
|
123
|
+
|
|
124
|
+
.hero {
|
|
125
|
+
text-align: center;
|
|
126
|
+
padding: 5rem 1rem 4rem;
|
|
127
|
+
max-width: 680px;
|
|
128
|
+
margin: 0 auto;
|
|
129
|
+
}
|
|
130
|
+
.hero-badge {
|
|
131
|
+
display: inline-block;
|
|
132
|
+
background: #1e1a2e;
|
|
133
|
+
border: 1px solid #4c1d95;
|
|
134
|
+
color: #a78bfa;
|
|
135
|
+
font-size: 0.75rem;
|
|
136
|
+
font-weight: 600;
|
|
137
|
+
letter-spacing: 0.05em;
|
|
138
|
+
text-transform: uppercase;
|
|
139
|
+
padding: 0.3rem 0.8rem;
|
|
140
|
+
border-radius: 999px;
|
|
141
|
+
margin-bottom: 1.5rem;
|
|
142
|
+
}
|
|
143
|
+
.hero h1 {
|
|
144
|
+
font-size: clamp(2rem, 5vw, 3.25rem);
|
|
145
|
+
font-weight: 800;
|
|
146
|
+
letter-spacing: -0.03em;
|
|
147
|
+
line-height: 1.15;
|
|
148
|
+
margin-bottom: 1rem;
|
|
149
|
+
}
|
|
150
|
+
.hero h1 span { color: #a78bfa; }
|
|
151
|
+
.hero p {
|
|
152
|
+
font-size: 1.1rem;
|
|
153
|
+
color: #94a3b8;
|
|
154
|
+
line-height: 1.7;
|
|
155
|
+
margin-bottom: 2.5rem;
|
|
156
|
+
}
|
|
157
|
+
.hero-actions { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; }
|
|
158
|
+
.btn {
|
|
159
|
+
display: inline-flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
gap: 0.4rem;
|
|
162
|
+
padding: 0.65rem 1.4rem;
|
|
163
|
+
border-radius: 8px;
|
|
164
|
+
font-size: 0.9rem;
|
|
165
|
+
font-weight: 600;
|
|
166
|
+
cursor: pointer;
|
|
167
|
+
border: none;
|
|
168
|
+
transition: opacity 0.15s, transform 0.1s;
|
|
169
|
+
text-decoration: none;
|
|
170
|
+
}
|
|
171
|
+
.btn:hover { opacity: 0.85; transform: translateY(-1px); }
|
|
172
|
+
.btn:active { transform: translateY(0); }
|
|
173
|
+
.btn-primary { background: #7c3aed; color: #fff; }
|
|
174
|
+
.btn-ghost { background: #1e1e2e; color: #e2e8f0; border: 1px solid #2d2d3e; }
|
|
175
|
+
|
|
176
|
+
.features {
|
|
177
|
+
display: grid;
|
|
178
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
179
|
+
gap: 1rem;
|
|
180
|
+
max-width: 900px;
|
|
181
|
+
margin: 0 auto 4rem;
|
|
182
|
+
padding: 0 1.5rem;
|
|
183
|
+
}
|
|
184
|
+
.card {
|
|
185
|
+
background: #16161f;
|
|
186
|
+
border: 1px solid #1e1e2e;
|
|
187
|
+
border-radius: 12px;
|
|
188
|
+
padding: 1.5rem;
|
|
189
|
+
}
|
|
190
|
+
.card-icon { font-size: 1.5rem; margin-bottom: 0.75rem; }
|
|
191
|
+
.card h3 { font-size: 0.95rem; font-weight: 700; margin-bottom: 0.4rem; }
|
|
192
|
+
.card p { font-size: 0.85rem; color: #64748b; line-height: 1.6; }
|
|
193
|
+
|
|
194
|
+
.demo {
|
|
195
|
+
max-width: 480px;
|
|
196
|
+
margin: 0 auto 4rem;
|
|
197
|
+
padding: 0 1.5rem;
|
|
198
|
+
text-align: center;
|
|
199
|
+
}
|
|
200
|
+
.demo-box {
|
|
201
|
+
background: #16161f;
|
|
202
|
+
border: 1px solid #1e1e2e;
|
|
203
|
+
border-radius: 12px;
|
|
204
|
+
padding: 2rem;
|
|
205
|
+
}
|
|
206
|
+
.demo-box h2 { font-size: 0.8rem; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 1.25rem; }
|
|
207
|
+
.counter { font-size: 3.5rem; font-weight: 800; color: #a78bfa; letter-spacing: -0.04em; margin-bottom: 1.25rem; }
|
|
208
|
+
.demo-actions { display: flex; gap: 0.75rem; justify-content: center; }
|
|
209
|
+
|
|
210
|
+
\`;
|
|
211
|
+
|
|
212
|
+
const App: HadarsApp<{}> = ({ context }) => {
|
|
213
|
+
const [count, setCount] = React.useState(0);
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<HadarsContext context={context}>
|
|
217
|
+
<HadarsHead status={200}>
|
|
218
|
+
<title>My App</title>
|
|
219
|
+
<meta id="viewport" name="viewport" content="width=device-width, initial-scale=1" />
|
|
220
|
+
<style id="app-styles" dangerouslySetInnerHTML={{ __html: css }} />
|
|
221
|
+
</HadarsHead>
|
|
222
|
+
|
|
223
|
+
<nav className="nav">
|
|
224
|
+
<span className="nav-brand">my-app</span>
|
|
225
|
+
<div className="nav-links">
|
|
226
|
+
<a href="https://github.com/dpostolachi/hadar" target="_blank" rel="noopener">github</a>
|
|
227
|
+
</div>
|
|
228
|
+
</nav>
|
|
229
|
+
|
|
230
|
+
<section className="hero">
|
|
231
|
+
<div className="hero-badge">built with hadars</div>
|
|
232
|
+
<h1>Ship <span>React apps</span><br />at full speed</h1>
|
|
233
|
+
<p>
|
|
234
|
+
SSR out of the box, zero config, instant hot-reload.
|
|
235
|
+
Edit <code>src/App.tsx</code> to get started.
|
|
236
|
+
</p>
|
|
237
|
+
<div className="hero-actions">
|
|
238
|
+
<button className="btn btn-primary" onClick={() => setCount(c => c + 1)}>
|
|
239
|
+
Try the counter ↓
|
|
240
|
+
</button>
|
|
241
|
+
</div>
|
|
242
|
+
</section>
|
|
243
|
+
|
|
244
|
+
<div className="features">
|
|
245
|
+
<div className="card">
|
|
246
|
+
<div className="card-icon">⚡</div>
|
|
247
|
+
<h3>Server-side rendering</h3>
|
|
248
|
+
<p>Pages render on the server and hydrate on the client — great for SEO and first paint.</p>
|
|
249
|
+
</div>
|
|
250
|
+
<div className="card">
|
|
251
|
+
<div className="card-icon">🔥</div>
|
|
252
|
+
<h3>Hot module reload</h3>
|
|
253
|
+
<p>Changes in <code>src/App.tsx</code> reflect instantly in the browser during development.</p>
|
|
254
|
+
</div>
|
|
255
|
+
<div className="card">
|
|
256
|
+
<div className="card-icon">📦</div>
|
|
257
|
+
<h3>Zero config</h3>
|
|
258
|
+
<p>One config file. Export a React component, run <code>hadars dev</code>, done.</p>
|
|
259
|
+
</div>
|
|
260
|
+
<div className="card">
|
|
261
|
+
<div className="card-icon">🗄️</div>
|
|
262
|
+
<h3>Server data hooks</h3>
|
|
263
|
+
<p>Use <code>useServerData</code> to fetch data on the server without extra round-trips.</p>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<div className="demo">
|
|
268
|
+
<div className="demo-box">
|
|
269
|
+
<h2>Client interactivity works</h2>
|
|
270
|
+
<div className="counter">{count}</div>
|
|
271
|
+
<div className="demo-actions">
|
|
272
|
+
<button className="btn btn-ghost" onClick={() => setCount(c => c - 1)}>− dec</button>
|
|
273
|
+
<button className="btn btn-primary" onClick={() => setCount(c => c + 1)}>+ inc</button>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
</HadarsContext>
|
|
279
|
+
);
|
|
280
|
+
};
|
|
113
281
|
|
|
114
282
|
export default App;
|
|
115
283
|
`,
|
package/dist/cli.js
CHANGED
|
@@ -892,15 +892,6 @@ var getConfigBase = (mode) => {
|
|
|
892
892
|
},
|
|
893
893
|
module: {
|
|
894
894
|
rules: [
|
|
895
|
-
{
|
|
896
|
-
test: /\.mdx?$/,
|
|
897
|
-
use: [
|
|
898
|
-
{
|
|
899
|
-
loader: "@mdx-js/loader",
|
|
900
|
-
options: {}
|
|
901
|
-
}
|
|
902
|
-
]
|
|
903
|
-
},
|
|
904
895
|
{
|
|
905
896
|
test: /\.css$/,
|
|
906
897
|
use: ["postcss-loader"],
|
|
@@ -1028,6 +1019,9 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
|
|
|
1028
1019
|
}
|
|
1029
1020
|
}
|
|
1030
1021
|
}
|
|
1022
|
+
if (opts.moduleRules && opts.moduleRules.length > 0) {
|
|
1023
|
+
localConfig.module.rules.push(...opts.moduleRules);
|
|
1024
|
+
}
|
|
1031
1025
|
const isServerBuild = Boolean(
|
|
1032
1026
|
opts.output && typeof opts.output === "object" && (opts.output.library || String(opts.output.filename || "").includes("ssr"))
|
|
1033
1027
|
);
|
|
@@ -1790,6 +1784,7 @@ var dev = async (options) => {
|
|
|
1790
1784
|
mode: "development",
|
|
1791
1785
|
swcPlugins: options.swcPlugins,
|
|
1792
1786
|
define: options.define,
|
|
1787
|
+
moduleRules: options.moduleRules,
|
|
1793
1788
|
htmlTemplate: resolvedHtmlTemplate
|
|
1794
1789
|
});
|
|
1795
1790
|
const devServer = new RspackDevServer({
|
|
@@ -1826,7 +1821,8 @@ var dev = async (options) => {
|
|
|
1826
1821
|
`--outFile=${SSR_FILENAME}`,
|
|
1827
1822
|
`--base=${baseURL}`,
|
|
1828
1823
|
...options.swcPlugins ? [`--swcPlugins=${JSON.stringify(options.swcPlugins)}`] : [],
|
|
1829
|
-
...options.define ? [`--define=${JSON.stringify(options.define)}`] : []
|
|
1824
|
+
...options.define ? [`--define=${JSON.stringify(options.define)}`] : [],
|
|
1825
|
+
...options.moduleRules ? [`--moduleRules=${JSON.stringify(options.moduleRules)}`] : []
|
|
1830
1826
|
], { stdio: "pipe" });
|
|
1831
1827
|
child.stdin?.end();
|
|
1832
1828
|
const cleanupChild = () => {
|
|
@@ -2006,6 +2002,7 @@ var build = async (options) => {
|
|
|
2006
2002
|
mode: "production",
|
|
2007
2003
|
swcPlugins: options.swcPlugins,
|
|
2008
2004
|
define: options.define,
|
|
2005
|
+
moduleRules: options.moduleRules,
|
|
2009
2006
|
optimization: options.optimization,
|
|
2010
2007
|
htmlTemplate: resolvedHtmlTemplate
|
|
2011
2008
|
}),
|
|
@@ -2021,7 +2018,8 @@ var build = async (options) => {
|
|
|
2021
2018
|
target: "node",
|
|
2022
2019
|
mode: "production",
|
|
2023
2020
|
swcPlugins: options.swcPlugins,
|
|
2024
|
-
define: options.define
|
|
2021
|
+
define: options.define,
|
|
2022
|
+
moduleRules: options.moduleRules
|
|
2025
2023
|
})
|
|
2026
2024
|
]);
|
|
2027
2025
|
await fs.rm(tmpFilePath);
|
|
@@ -2212,17 +2210,185 @@ dist/
|
|
|
2212
2210
|
"src/App.tsx": () => `import React from 'react';
|
|
2213
2211
|
import { HadarsContext, HadarsHead, type HadarsApp } from 'hadars';
|
|
2214
2212
|
|
|
2215
|
-
const
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2213
|
+
const css = \`
|
|
2214
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2215
|
+
|
|
2216
|
+
body {
|
|
2217
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2218
|
+
background: #0f0f13;
|
|
2219
|
+
color: #e2e8f0;
|
|
2220
|
+
min-height: 100vh;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
.nav {
|
|
2224
|
+
display: flex;
|
|
2225
|
+
align-items: center;
|
|
2226
|
+
justify-content: space-between;
|
|
2227
|
+
padding: 1rem 2rem;
|
|
2228
|
+
border-bottom: 1px solid #1e1e2e;
|
|
2229
|
+
}
|
|
2230
|
+
.nav-brand { font-weight: 700; font-size: 1.1rem; color: #a78bfa; letter-spacing: -0.02em; }
|
|
2231
|
+
.nav-links { display: flex; gap: 1.5rem; }
|
|
2232
|
+
.nav-links a { color: #94a3b8; text-decoration: none; font-size: 0.9rem; }
|
|
2233
|
+
.nav-links a:hover { color: #e2e8f0; }
|
|
2234
|
+
|
|
2235
|
+
.hero {
|
|
2236
|
+
text-align: center;
|
|
2237
|
+
padding: 5rem 1rem 4rem;
|
|
2238
|
+
max-width: 680px;
|
|
2239
|
+
margin: 0 auto;
|
|
2240
|
+
}
|
|
2241
|
+
.hero-badge {
|
|
2242
|
+
display: inline-block;
|
|
2243
|
+
background: #1e1a2e;
|
|
2244
|
+
border: 1px solid #4c1d95;
|
|
2245
|
+
color: #a78bfa;
|
|
2246
|
+
font-size: 0.75rem;
|
|
2247
|
+
font-weight: 600;
|
|
2248
|
+
letter-spacing: 0.05em;
|
|
2249
|
+
text-transform: uppercase;
|
|
2250
|
+
padding: 0.3rem 0.8rem;
|
|
2251
|
+
border-radius: 999px;
|
|
2252
|
+
margin-bottom: 1.5rem;
|
|
2253
|
+
}
|
|
2254
|
+
.hero h1 {
|
|
2255
|
+
font-size: clamp(2rem, 5vw, 3.25rem);
|
|
2256
|
+
font-weight: 800;
|
|
2257
|
+
letter-spacing: -0.03em;
|
|
2258
|
+
line-height: 1.15;
|
|
2259
|
+
margin-bottom: 1rem;
|
|
2260
|
+
}
|
|
2261
|
+
.hero h1 span { color: #a78bfa; }
|
|
2262
|
+
.hero p {
|
|
2263
|
+
font-size: 1.1rem;
|
|
2264
|
+
color: #94a3b8;
|
|
2265
|
+
line-height: 1.7;
|
|
2266
|
+
margin-bottom: 2.5rem;
|
|
2267
|
+
}
|
|
2268
|
+
.hero-actions { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; }
|
|
2269
|
+
.btn {
|
|
2270
|
+
display: inline-flex;
|
|
2271
|
+
align-items: center;
|
|
2272
|
+
gap: 0.4rem;
|
|
2273
|
+
padding: 0.65rem 1.4rem;
|
|
2274
|
+
border-radius: 8px;
|
|
2275
|
+
font-size: 0.9rem;
|
|
2276
|
+
font-weight: 600;
|
|
2277
|
+
cursor: pointer;
|
|
2278
|
+
border: none;
|
|
2279
|
+
transition: opacity 0.15s, transform 0.1s;
|
|
2280
|
+
text-decoration: none;
|
|
2281
|
+
}
|
|
2282
|
+
.btn:hover { opacity: 0.85; transform: translateY(-1px); }
|
|
2283
|
+
.btn:active { transform: translateY(0); }
|
|
2284
|
+
.btn-primary { background: #7c3aed; color: #fff; }
|
|
2285
|
+
.btn-ghost { background: #1e1e2e; color: #e2e8f0; border: 1px solid #2d2d3e; }
|
|
2286
|
+
|
|
2287
|
+
.features {
|
|
2288
|
+
display: grid;
|
|
2289
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
2290
|
+
gap: 1rem;
|
|
2291
|
+
max-width: 900px;
|
|
2292
|
+
margin: 0 auto 4rem;
|
|
2293
|
+
padding: 0 1.5rem;
|
|
2294
|
+
}
|
|
2295
|
+
.card {
|
|
2296
|
+
background: #16161f;
|
|
2297
|
+
border: 1px solid #1e1e2e;
|
|
2298
|
+
border-radius: 12px;
|
|
2299
|
+
padding: 1.5rem;
|
|
2300
|
+
}
|
|
2301
|
+
.card-icon { font-size: 1.5rem; margin-bottom: 0.75rem; }
|
|
2302
|
+
.card h3 { font-size: 0.95rem; font-weight: 700; margin-bottom: 0.4rem; }
|
|
2303
|
+
.card p { font-size: 0.85rem; color: #64748b; line-height: 1.6; }
|
|
2304
|
+
|
|
2305
|
+
.demo {
|
|
2306
|
+
max-width: 480px;
|
|
2307
|
+
margin: 0 auto 4rem;
|
|
2308
|
+
padding: 0 1.5rem;
|
|
2309
|
+
text-align: center;
|
|
2310
|
+
}
|
|
2311
|
+
.demo-box {
|
|
2312
|
+
background: #16161f;
|
|
2313
|
+
border: 1px solid #1e1e2e;
|
|
2314
|
+
border-radius: 12px;
|
|
2315
|
+
padding: 2rem;
|
|
2316
|
+
}
|
|
2317
|
+
.demo-box h2 { font-size: 0.8rem; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 1.25rem; }
|
|
2318
|
+
.counter { font-size: 3.5rem; font-weight: 800; color: #a78bfa; letter-spacing: -0.04em; margin-bottom: 1.25rem; }
|
|
2319
|
+
.demo-actions { display: flex; gap: 0.75rem; justify-content: center; }
|
|
2320
|
+
|
|
2321
|
+
\`;
|
|
2322
|
+
|
|
2323
|
+
const App: HadarsApp<{}> = ({ context }) => {
|
|
2324
|
+
const [count, setCount] = React.useState(0);
|
|
2325
|
+
|
|
2326
|
+
return (
|
|
2327
|
+
<HadarsContext context={context}>
|
|
2328
|
+
<HadarsHead status={200}>
|
|
2329
|
+
<title>My App</title>
|
|
2330
|
+
<meta id="viewport" name="viewport" content="width=device-width, initial-scale=1" />
|
|
2331
|
+
<style id="app-styles" dangerouslySetInnerHTML={{ __html: css }} />
|
|
2332
|
+
</HadarsHead>
|
|
2333
|
+
|
|
2334
|
+
<nav className="nav">
|
|
2335
|
+
<span className="nav-brand">my-app</span>
|
|
2336
|
+
<div className="nav-links">
|
|
2337
|
+
<a href="https://github.com/dpostolachi/hadar" target="_blank" rel="noopener">github</a>
|
|
2338
|
+
</div>
|
|
2339
|
+
</nav>
|
|
2340
|
+
|
|
2341
|
+
<section className="hero">
|
|
2342
|
+
<div className="hero-badge">built with hadars</div>
|
|
2343
|
+
<h1>Ship <span>React apps</span><br />at full speed</h1>
|
|
2344
|
+
<p>
|
|
2345
|
+
SSR out of the box, zero config, instant hot-reload.
|
|
2346
|
+
Edit <code>src/App.tsx</code> to get started.
|
|
2347
|
+
</p>
|
|
2348
|
+
<div className="hero-actions">
|
|
2349
|
+
<button className="btn btn-primary" onClick={() => setCount(c => c + 1)}>
|
|
2350
|
+
Try the counter \u2193
|
|
2351
|
+
</button>
|
|
2352
|
+
</div>
|
|
2353
|
+
</section>
|
|
2354
|
+
|
|
2355
|
+
<div className="features">
|
|
2356
|
+
<div className="card">
|
|
2357
|
+
<div className="card-icon">\u26A1</div>
|
|
2358
|
+
<h3>Server-side rendering</h3>
|
|
2359
|
+
<p>Pages render on the server and hydrate on the client \u2014 great for SEO and first paint.</p>
|
|
2360
|
+
</div>
|
|
2361
|
+
<div className="card">
|
|
2362
|
+
<div className="card-icon">\u{1F525}</div>
|
|
2363
|
+
<h3>Hot module reload</h3>
|
|
2364
|
+
<p>Changes in <code>src/App.tsx</code> reflect instantly in the browser during development.</p>
|
|
2365
|
+
</div>
|
|
2366
|
+
<div className="card">
|
|
2367
|
+
<div className="card-icon">\u{1F4E6}</div>
|
|
2368
|
+
<h3>Zero config</h3>
|
|
2369
|
+
<p>One config file. Export a React component, run <code>hadars dev</code>, done.</p>
|
|
2370
|
+
</div>
|
|
2371
|
+
<div className="card">
|
|
2372
|
+
<div className="card-icon">\u{1F5C4}\uFE0F</div>
|
|
2373
|
+
<h3>Server data hooks</h3>
|
|
2374
|
+
<p>Use <code>useServerData</code> to fetch data on the server without extra round-trips.</p>
|
|
2375
|
+
</div>
|
|
2376
|
+
</div>
|
|
2377
|
+
|
|
2378
|
+
<div className="demo">
|
|
2379
|
+
<div className="demo-box">
|
|
2380
|
+
<h2>Client interactivity works</h2>
|
|
2381
|
+
<div className="counter">{count}</div>
|
|
2382
|
+
<div className="demo-actions">
|
|
2383
|
+
<button className="btn btn-ghost" onClick={() => setCount(c => c - 1)}>\u2212 dec</button>
|
|
2384
|
+
<button className="btn btn-primary" onClick={() => setCount(c => c + 1)}>+ inc</button>
|
|
2385
|
+
</div>
|
|
2386
|
+
</div>
|
|
2387
|
+
</div>
|
|
2388
|
+
|
|
2389
|
+
</HadarsContext>
|
|
2390
|
+
);
|
|
2391
|
+
};
|
|
2226
2392
|
|
|
2227
2393
|
export default App;
|
|
2228
2394
|
`
|
package/dist/index.d.ts
CHANGED
|
@@ -111,6 +111,22 @@ interface HadarsOptions {
|
|
|
111
111
|
* Note: inline styles are processed once at startup and are not live-reloaded.
|
|
112
112
|
*/
|
|
113
113
|
htmlTemplate?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Additional rspack module rules appended to the built-in rule set.
|
|
116
|
+
* Applied to both the client and the SSR bundle.
|
|
117
|
+
*
|
|
118
|
+
* Useful for loaders not included by default, such as `@mdx-js/loader`,
|
|
119
|
+
* `less-loader`, `yaml-loader`, etc.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* moduleRules: [
|
|
123
|
+
* {
|
|
124
|
+
* test: /\.mdx?$/,
|
|
125
|
+
* use: [{ loader: '@mdx-js/loader' }],
|
|
126
|
+
* },
|
|
127
|
+
* ]
|
|
128
|
+
*/
|
|
129
|
+
moduleRules?: Record<string, any>[];
|
|
114
130
|
/**
|
|
115
131
|
* SSR response cache for `run()` mode. Has no effect in `dev()` mode.
|
|
116
132
|
*
|
package/dist/ssr-watch.js
CHANGED
|
@@ -32,15 +32,6 @@ var getConfigBase = (mode) => {
|
|
|
32
32
|
},
|
|
33
33
|
module: {
|
|
34
34
|
rules: [
|
|
35
|
-
{
|
|
36
|
-
test: /\.mdx?$/,
|
|
37
|
-
use: [
|
|
38
|
-
{
|
|
39
|
-
loader: "@mdx-js/loader",
|
|
40
|
-
options: {}
|
|
41
|
-
}
|
|
42
|
-
]
|
|
43
|
-
},
|
|
44
35
|
{
|
|
45
36
|
test: /\.css$/,
|
|
46
37
|
use: ["postcss-loader"],
|
|
@@ -168,6 +159,9 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
|
|
|
168
159
|
}
|
|
169
160
|
}
|
|
170
161
|
}
|
|
162
|
+
if (opts.moduleRules && opts.moduleRules.length > 0) {
|
|
163
|
+
localConfig.module.rules.push(...opts.moduleRules);
|
|
164
|
+
}
|
|
171
165
|
const isServerBuild = Boolean(
|
|
172
166
|
opts.output && typeof opts.output === "object" && (opts.output.library || String(opts.output.filename || "").includes("ssr"))
|
|
173
167
|
);
|
|
@@ -360,6 +354,7 @@ var outFile = argv["outFile"] || "index.ssr.js";
|
|
|
360
354
|
var base = argv["base"] || "";
|
|
361
355
|
var swcPlugins = argv["swcPlugins"] ? JSON.parse(argv["swcPlugins"]) : void 0;
|
|
362
356
|
var define = argv["define"] ? JSON.parse(argv["define"]) : void 0;
|
|
357
|
+
var moduleRules = argv["moduleRules"] ? JSON.parse(argv["moduleRules"]) : void 0;
|
|
363
358
|
if (!entry) {
|
|
364
359
|
console.error("ssr-watch: missing --entry argument");
|
|
365
360
|
process.exit(1);
|
|
@@ -381,6 +376,7 @@ if (!entry) {
|
|
|
381
376
|
watch: true,
|
|
382
377
|
swcPlugins,
|
|
383
378
|
define,
|
|
379
|
+
moduleRules,
|
|
384
380
|
onChange: () => {
|
|
385
381
|
console.log("ssr-watch: SSR rebuilt");
|
|
386
382
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hadars",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "Minimal SSR framework for React — rspack, HMR, TypeScript, Bun/Node/Deno",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -77,7 +77,6 @@
|
|
|
77
77
|
"typescript": "^5.9.3"
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
|
-
"@mdx-js/loader": "^3.1.1",
|
|
81
80
|
"@svgr/webpack": "^8.1.0",
|
|
82
81
|
"postcss": "^8.5.8",
|
|
83
82
|
"postcss-load-config": "^6.0.1",
|
package/src/build.ts
CHANGED
|
@@ -507,6 +507,7 @@ export const dev = async (options: HadarsRuntimeOptions) => {
|
|
|
507
507
|
mode: 'development',
|
|
508
508
|
swcPlugins: options.swcPlugins,
|
|
509
509
|
define: options.define,
|
|
510
|
+
moduleRules: options.moduleRules,
|
|
510
511
|
htmlTemplate: resolvedHtmlTemplate,
|
|
511
512
|
});
|
|
512
513
|
|
|
@@ -554,6 +555,7 @@ export const dev = async (options: HadarsRuntimeOptions) => {
|
|
|
554
555
|
`--base=${baseURL}`,
|
|
555
556
|
...(options.swcPlugins ? [`--swcPlugins=${JSON.stringify(options.swcPlugins)}`] : []),
|
|
556
557
|
...(options.define ? [`--define=${JSON.stringify(options.define)}`] : []),
|
|
558
|
+
...(options.moduleRules ? [`--moduleRules=${JSON.stringify(options.moduleRules)}`] : []),
|
|
557
559
|
], { stdio: 'pipe' });
|
|
558
560
|
child.stdin?.end();
|
|
559
561
|
|
|
@@ -747,6 +749,7 @@ export const build = async (options: HadarsRuntimeOptions) => {
|
|
|
747
749
|
mode: 'production',
|
|
748
750
|
swcPlugins: options.swcPlugins,
|
|
749
751
|
define: options.define,
|
|
752
|
+
moduleRules: options.moduleRules,
|
|
750
753
|
optimization: options.optimization,
|
|
751
754
|
htmlTemplate: resolvedHtmlTemplate,
|
|
752
755
|
}),
|
|
@@ -763,6 +766,7 @@ export const build = async (options: HadarsRuntimeOptions) => {
|
|
|
763
766
|
mode: 'production',
|
|
764
767
|
swcPlugins: options.swcPlugins,
|
|
765
768
|
define: options.define,
|
|
769
|
+
moduleRules: options.moduleRules,
|
|
766
770
|
}),
|
|
767
771
|
]);
|
|
768
772
|
await fs.rm(tmpFilePath);
|
package/src/ssr-watch.ts
CHANGED
|
@@ -21,6 +21,7 @@ const outFile = argv['outFile'] || 'index.ssr.js';
|
|
|
21
21
|
const base = argv['base'] || '';
|
|
22
22
|
const swcPlugins = argv['swcPlugins'] ? JSON.parse(argv['swcPlugins']) : undefined;
|
|
23
23
|
const define = argv['define'] ? JSON.parse(argv['define']) : undefined;
|
|
24
|
+
const moduleRules = argv['moduleRules'] ? JSON.parse(argv['moduleRules']) : undefined;
|
|
24
25
|
|
|
25
26
|
if (!entry) {
|
|
26
27
|
console.error('ssr-watch: missing --entry argument');
|
|
@@ -44,6 +45,7 @@ if (!entry) {
|
|
|
44
45
|
watch: true,
|
|
45
46
|
swcPlugins,
|
|
46
47
|
define,
|
|
48
|
+
moduleRules,
|
|
47
49
|
onChange: () => {
|
|
48
50
|
console.log('ssr-watch: SSR rebuilt');
|
|
49
51
|
}
|
package/src/types/hadars.ts
CHANGED
|
@@ -111,6 +111,22 @@ export interface HadarsOptions {
|
|
|
111
111
|
* Note: inline styles are processed once at startup and are not live-reloaded.
|
|
112
112
|
*/
|
|
113
113
|
htmlTemplate?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Additional rspack module rules appended to the built-in rule set.
|
|
116
|
+
* Applied to both the client and the SSR bundle.
|
|
117
|
+
*
|
|
118
|
+
* Useful for loaders not included by default, such as `@mdx-js/loader`,
|
|
119
|
+
* `less-loader`, `yaml-loader`, etc.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* moduleRules: [
|
|
123
|
+
* {
|
|
124
|
+
* test: /\.mdx?$/,
|
|
125
|
+
* use: [{ loader: '@mdx-js/loader' }],
|
|
126
|
+
* },
|
|
127
|
+
* ]
|
|
128
|
+
*/
|
|
129
|
+
moduleRules?: Record<string, any>[];
|
|
114
130
|
/**
|
|
115
131
|
* SSR response cache for `run()` mode. Has no effect in `dev()` mode.
|
|
116
132
|
*
|
package/src/utils/rspack.ts
CHANGED
|
@@ -38,16 +38,6 @@ const getConfigBase = (mode: "development" | "production"): Omit<Configuration,
|
|
|
38
38
|
},
|
|
39
39
|
module: {
|
|
40
40
|
rules: [
|
|
41
|
-
{
|
|
42
|
-
test: /\.mdx?$/,
|
|
43
|
-
use: [
|
|
44
|
-
{
|
|
45
|
-
loader: '@mdx-js/loader',
|
|
46
|
-
options: {
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
},
|
|
51
41
|
{
|
|
52
42
|
test: /\.css$/,
|
|
53
43
|
use: ["postcss-loader"],
|
|
@@ -142,6 +132,8 @@ interface EntryOptions {
|
|
|
142
132
|
base?: string;
|
|
143
133
|
// optional rspack optimization overrides (production client builds only)
|
|
144
134
|
optimization?: Record<string, unknown>;
|
|
135
|
+
// additional module rules appended after the built-in rules
|
|
136
|
+
moduleRules?: Record<string, any>[];
|
|
145
137
|
}
|
|
146
138
|
|
|
147
139
|
const buildCompilerConfig = (
|
|
@@ -207,6 +199,10 @@ const buildCompilerConfig = (
|
|
|
207
199
|
}
|
|
208
200
|
}
|
|
209
201
|
|
|
202
|
+
if (opts.moduleRules && opts.moduleRules.length > 0) {
|
|
203
|
+
localConfig.module.rules.push(...opts.moduleRules);
|
|
204
|
+
}
|
|
205
|
+
|
|
210
206
|
// For server (SSR) builds we should avoid bundling react/react-dom so
|
|
211
207
|
// the runtime uses the same React instance as the host. If the output
|
|
212
208
|
// is a library/module (i.e. `opts.output.library` present or filename
|