hadars 0.1.20 → 0.1.22
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 +197 -31
- package/dist/index.d.ts +16 -0
- package/dist/ssr-watch.js +14 -18
- 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 +9 -13
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"],
|
|
@@ -935,7 +926,7 @@ var getConfigBase = (mode) => {
|
|
|
935
926
|
react: {
|
|
936
927
|
runtime: "automatic",
|
|
937
928
|
development: isDev,
|
|
938
|
-
refresh: isDev
|
|
929
|
+
refresh: isDev && !isServerBuild
|
|
939
930
|
}
|
|
940
931
|
}
|
|
941
932
|
}
|
|
@@ -966,7 +957,7 @@ var getConfigBase = (mode) => {
|
|
|
966
957
|
react: {
|
|
967
958
|
runtime: "automatic",
|
|
968
959
|
development: isDev,
|
|
969
|
-
refresh: isDev
|
|
960
|
+
refresh: isDev && !isServerBuild
|
|
970
961
|
}
|
|
971
962
|
}
|
|
972
963
|
}
|
|
@@ -1028,12 +1019,15 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
|
|
|
1028
1019
|
}
|
|
1029
1020
|
}
|
|
1030
1021
|
}
|
|
1031
|
-
|
|
1022
|
+
if (opts.moduleRules && opts.moduleRules.length > 0) {
|
|
1023
|
+
localConfig.module.rules.push(...opts.moduleRules);
|
|
1024
|
+
}
|
|
1025
|
+
const isServerBuild2 = Boolean(
|
|
1032
1026
|
opts.output && typeof opts.output === "object" && (opts.output.library || String(opts.output.filename || "").includes("ssr"))
|
|
1033
1027
|
);
|
|
1034
1028
|
const slimReactIndex = pathMod.resolve(packageDir, "slim-react", "index.js");
|
|
1035
1029
|
const slimReactJsx = pathMod.resolve(packageDir, "slim-react", "jsx-runtime.js");
|
|
1036
|
-
const resolveAliases =
|
|
1030
|
+
const resolveAliases = isServerBuild2 ? {
|
|
1037
1031
|
// Route all React imports to slim-react for SSR.
|
|
1038
1032
|
react: slimReactIndex,
|
|
1039
1033
|
"react/jsx-runtime": slimReactJsx,
|
|
@@ -1044,7 +1038,7 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
|
|
|
1044
1038
|
"@emotion/cache": path.resolve(process.cwd(), "node_modules", "@emotion", "cache"),
|
|
1045
1039
|
"@emotion/styled": path.resolve(process.cwd(), "node_modules", "@emotion", "styled")
|
|
1046
1040
|
} : void 0;
|
|
1047
|
-
const externals =
|
|
1041
|
+
const externals = isServerBuild2 ? [
|
|
1048
1042
|
// react / react-dom are replaced by slim-react via alias above — not external.
|
|
1049
1043
|
// emotion should be external on server builds to avoid client/browser code
|
|
1050
1044
|
"@emotion/react",
|
|
@@ -1065,9 +1059,9 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
|
|
|
1065
1059
|
extensions: [".tsx", ".ts", ".js", ".jsx"],
|
|
1066
1060
|
alias: resolveAliases,
|
|
1067
1061
|
// for server builds prefer the package "main"/"module" fields and avoid "browser" so we don't pick browser-specific entrypoints
|
|
1068
|
-
mainFields:
|
|
1062
|
+
mainFields: isServerBuild2 ? ["main", "module"] : ["browser", "module", "main"]
|
|
1069
1063
|
};
|
|
1070
|
-
const optimization = !
|
|
1064
|
+
const optimization = !isServerBuild2 && !isDev ? {
|
|
1071
1065
|
moduleIds: "deterministic",
|
|
1072
1066
|
splitChunks: {
|
|
1073
1067
|
chunks: "all",
|
|
@@ -1127,7 +1121,7 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
|
|
|
1127
1121
|
});
|
|
1128
1122
|
}
|
|
1129
1123
|
},
|
|
1130
|
-
isDev && new ReactRefreshPlugin(),
|
|
1124
|
+
isDev && !isServerBuild2 && new ReactRefreshPlugin(),
|
|
1131
1125
|
includeHotPlugin && isDev && new rspack.HotModuleReplacementPlugin(),
|
|
1132
1126
|
...extraPlugins
|
|
1133
1127
|
],
|
|
@@ -1143,7 +1137,7 @@ var buildCompilerConfig = (entry, opts, includeHotPlugin) => {
|
|
|
1143
1137
|
// for client builds. SSR builds still need it for dynamic import() of exports.
|
|
1144
1138
|
experiments: {
|
|
1145
1139
|
...localConfig.experiments || {},
|
|
1146
|
-
outputModule:
|
|
1140
|
+
outputModule: isServerBuild2
|
|
1147
1141
|
},
|
|
1148
1142
|
// Prevent rspack from watching its own build output — without this the
|
|
1149
1143
|
// SSR watcher writing .hadars/index.ssr.js triggers the client compiler
|
|
@@ -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"],
|
|
@@ -75,7 +66,7 @@ var getConfigBase = (mode) => {
|
|
|
75
66
|
react: {
|
|
76
67
|
runtime: "automatic",
|
|
77
68
|
development: isDev,
|
|
78
|
-
refresh: isDev
|
|
69
|
+
refresh: isDev && !isServerBuild
|
|
79
70
|
}
|
|
80
71
|
}
|
|
81
72
|
}
|
|
@@ -106,7 +97,7 @@ var getConfigBase = (mode) => {
|
|
|
106
97
|
react: {
|
|
107
98
|
runtime: "automatic",
|
|
108
99
|
development: isDev,
|
|
109
|
-
refresh: isDev
|
|
100
|
+
refresh: isDev && !isServerBuild
|
|
110
101
|
}
|
|
111
102
|
}
|
|
112
103
|
}
|
|
@@ -168,12 +159,15 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
|
|
|
168
159
|
}
|
|
169
160
|
}
|
|
170
161
|
}
|
|
171
|
-
|
|
162
|
+
if (opts.moduleRules && opts.moduleRules.length > 0) {
|
|
163
|
+
localConfig.module.rules.push(...opts.moduleRules);
|
|
164
|
+
}
|
|
165
|
+
const isServerBuild2 = Boolean(
|
|
172
166
|
opts.output && typeof opts.output === "object" && (opts.output.library || String(opts.output.filename || "").includes("ssr"))
|
|
173
167
|
);
|
|
174
168
|
const slimReactIndex = pathMod.resolve(packageDir, "slim-react", "index.js");
|
|
175
169
|
const slimReactJsx = pathMod.resolve(packageDir, "slim-react", "jsx-runtime.js");
|
|
176
|
-
const resolveAliases =
|
|
170
|
+
const resolveAliases = isServerBuild2 ? {
|
|
177
171
|
// Route all React imports to slim-react for SSR.
|
|
178
172
|
react: slimReactIndex,
|
|
179
173
|
"react/jsx-runtime": slimReactJsx,
|
|
@@ -184,7 +178,7 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
|
|
|
184
178
|
"@emotion/cache": path.resolve(process.cwd(), "node_modules", "@emotion", "cache"),
|
|
185
179
|
"@emotion/styled": path.resolve(process.cwd(), "node_modules", "@emotion", "styled")
|
|
186
180
|
} : void 0;
|
|
187
|
-
const externals =
|
|
181
|
+
const externals = isServerBuild2 ? [
|
|
188
182
|
// react / react-dom are replaced by slim-react via alias above — not external.
|
|
189
183
|
// emotion should be external on server builds to avoid client/browser code
|
|
190
184
|
"@emotion/react",
|
|
@@ -205,9 +199,9 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
|
|
|
205
199
|
extensions: [".tsx", ".ts", ".js", ".jsx"],
|
|
206
200
|
alias: resolveAliases,
|
|
207
201
|
// for server builds prefer the package "main"/"module" fields and avoid "browser" so we don't pick browser-specific entrypoints
|
|
208
|
-
mainFields:
|
|
202
|
+
mainFields: isServerBuild2 ? ["main", "module"] : ["browser", "module", "main"]
|
|
209
203
|
};
|
|
210
|
-
const optimization = !
|
|
204
|
+
const optimization = !isServerBuild2 && !isDev ? {
|
|
211
205
|
moduleIds: "deterministic",
|
|
212
206
|
splitChunks: {
|
|
213
207
|
chunks: "all",
|
|
@@ -267,7 +261,7 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
|
|
|
267
261
|
});
|
|
268
262
|
}
|
|
269
263
|
},
|
|
270
|
-
isDev && new ReactRefreshPlugin(),
|
|
264
|
+
isDev && !isServerBuild2 && new ReactRefreshPlugin(),
|
|
271
265
|
includeHotPlugin && isDev && new rspack.HotModuleReplacementPlugin(),
|
|
272
266
|
...extraPlugins
|
|
273
267
|
],
|
|
@@ -283,7 +277,7 @@ var buildCompilerConfig = (entry2, opts, includeHotPlugin) => {
|
|
|
283
277
|
// for client builds. SSR builds still need it for dynamic import() of exports.
|
|
284
278
|
experiments: {
|
|
285
279
|
...localConfig.experiments || {},
|
|
286
|
-
outputModule:
|
|
280
|
+
outputModule: isServerBuild2
|
|
287
281
|
},
|
|
288
282
|
// Prevent rspack from watching its own build output — without this the
|
|
289
283
|
// SSR watcher writing .hadars/index.ssr.js triggers the client compiler
|
|
@@ -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.22",
|
|
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"],
|
|
@@ -82,7 +72,7 @@ const getConfigBase = (mode: "development" | "production"): Omit<Configuration,
|
|
|
82
72
|
react: {
|
|
83
73
|
runtime: "automatic",
|
|
84
74
|
development: isDev,
|
|
85
|
-
refresh: isDev,
|
|
75
|
+
refresh: isDev && !isServerBuild,
|
|
86
76
|
},
|
|
87
77
|
},
|
|
88
78
|
},
|
|
@@ -113,7 +103,7 @@ const getConfigBase = (mode: "development" | "production"): Omit<Configuration,
|
|
|
113
103
|
react: {
|
|
114
104
|
runtime: "automatic",
|
|
115
105
|
development: isDev,
|
|
116
|
-
refresh: isDev,
|
|
106
|
+
refresh: isDev && !isServerBuild,
|
|
117
107
|
},
|
|
118
108
|
},
|
|
119
109
|
},
|
|
@@ -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
|
|
@@ -330,7 +326,7 @@ const buildCompilerConfig = (
|
|
|
330
326
|
});
|
|
331
327
|
},
|
|
332
328
|
},
|
|
333
|
-
isDev && new ReactRefreshPlugin(),
|
|
329
|
+
isDev && !isServerBuild && new ReactRefreshPlugin(),
|
|
334
330
|
includeHotPlugin && isDev && new rspack.HotModuleReplacementPlugin(),
|
|
335
331
|
...extraPlugins,
|
|
336
332
|
],
|