cindel 1.0.0
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 +675 -0
- package/README.md +546 -0
- package/dist/client/file-loader.d.ts +24 -0
- package/dist/client/file-loader.d.ts.map +1 -0
- package/dist/client/hmr-client.d.ts +185 -0
- package/dist/client/hmr-client.d.ts.map +1 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.iife.js +2232 -0
- package/dist/client.iife.js.map +7 -0
- package/dist/client.iife.min.js +2 -0
- package/dist/client.iife.min.js.map +7 -0
- package/dist/client.js +2221 -0
- package/dist/client.js.map +7 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1142 -0
- package/dist/index.js.map +7 -0
- package/dist/server/file-watcher.d.ts +93 -0
- package/dist/server/file-watcher.d.ts.map +1 -0
- package/dist/server/hmr-server.d.ts +378 -0
- package/dist/server/hmr-server.d.ts.map +1 -0
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.d.ts.map +1 -0
- package/dist/server/ws-proxy.d.ts +5 -0
- package/dist/server/ws-proxy.d.ts.map +1 -0
- package/dist/shared/constants.d.ts +24 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/logger.d.ts +39 -0
- package/dist/shared/logger.d.ts.map +1 -0
- package/dist/shared/utils.d.ts +13 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
## 
|
|
2
|
+
|
|
3
|
+
> Hot module replacement server and client with file watching, static file serving, CORS proxy and WebSocket proxy support
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/cindel)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://bundlephobia.com/package/cindel)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
**HMR & File Watching**
|
|
14
|
+
|
|
15
|
+
- Instant push driven HMR over WebSocket on file change
|
|
16
|
+
- Atomic CSS hot swap (no flash of unstyled content), script execution, and ES module reload
|
|
17
|
+
- Glob pattern support for watch, ignore, and cold file configuration
|
|
18
|
+
- Cold file patterns that can trigger a full page reload instead of HMR
|
|
19
|
+
- Override detection to map replacement files onto their originals
|
|
20
|
+
|
|
21
|
+
**Server**
|
|
22
|
+
|
|
23
|
+
- HTTP CORS proxy with configurable header injection
|
|
24
|
+
- WebSocket proxy with header forwarding and message interception
|
|
25
|
+
- Static file server and automatic `index.html` loader injection
|
|
26
|
+
- TLS/HTTPS + WSS support
|
|
27
|
+
- `/files` endpoint exposing the live watched file list as JSON
|
|
28
|
+
|
|
29
|
+
**Client**
|
|
30
|
+
|
|
31
|
+
- Exponential backoff with automatic reconnect
|
|
32
|
+
- No runtime dependencies, so it works in any modern browser
|
|
33
|
+
- Event system with `on`, `once`, and `off` for connect, disconnect, reload, add, remove, etc.
|
|
34
|
+
- IIFE build compatible with userscript managers (Tampermonkey, Greasemonkey) via `@require`
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
| Runtime | Version |
|
|
41
|
+
| ------- | -------- |
|
|
42
|
+
| Bun | >= 1.0.0 |
|
|
43
|
+
|
|
44
|
+
The server uses Bun's native `Bun.serve`, `Bun.file` and `Bun.Glob` APIs and is not compatible with Node.js. The browser client has no runtime dependencies and works in any modern browser.
|
|
45
|
+
|
|
46
|
+
> Note that only the changed file itself is re-executed on reload, changes do not propagate up the ES module import chain. TypeScript is not directly supported for the same reason.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
bun add cindel
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
// server.js
|
|
62
|
+
import { HMRServer } from "cindel/server";
|
|
63
|
+
|
|
64
|
+
const server = new HMRServer({
|
|
65
|
+
port: 1338,
|
|
66
|
+
watch: ["src"],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await server.start();
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
// browser - requires a bundler
|
|
74
|
+
import { HMRClient } from "cindel/client";
|
|
75
|
+
|
|
76
|
+
const client = new HMRClient({ port: 1338 });
|
|
77
|
+
await client.connect();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Or load it directly from a CDN with no bundler:
|
|
81
|
+
|
|
82
|
+
```html
|
|
83
|
+
<script src="https://cdn.jsdelivr.net/npm/cindel"></script>
|
|
84
|
+
<script>
|
|
85
|
+
const client = new HMR.HMRClient({ port: 1338 });
|
|
86
|
+
client.connect();
|
|
87
|
+
</script>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Another way with dynamic importing:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
(async () => {
|
|
94
|
+
const { HMRClient } =
|
|
95
|
+
await import("https://cdn.jsdelivr.net/npm/cindel/dist/client.js");
|
|
96
|
+
const client = new HMRClient({ port: 1338 });
|
|
97
|
+
await client.connect();
|
|
98
|
+
})();
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
You can even load it through a user script on any domain:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
// ==UserScript==
|
|
105
|
+
// @name Cindel loader
|
|
106
|
+
// @version 1.0
|
|
107
|
+
// @description Instead of making multiple scripts file you just inject them all locally
|
|
108
|
+
// @match https://example.com/*
|
|
109
|
+
// @require https://cdn.jsdelivr.net/npm/cindel
|
|
110
|
+
// @grant none
|
|
111
|
+
// ==/UserScript==
|
|
112
|
+
|
|
113
|
+
(async () => {
|
|
114
|
+
const client = new HMR.HMRClient({
|
|
115
|
+
port: 1338,
|
|
116
|
+
secure: true,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await client.connect();
|
|
120
|
+
})();
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Server
|
|
126
|
+
|
|
127
|
+
### `new HMRServer(options)`
|
|
128
|
+
|
|
129
|
+
| Option | Type | Default | Description |
|
|
130
|
+
| ---------------- | --------------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------- |
|
|
131
|
+
| `port` | `number` | `1338` | Port to listen on |
|
|
132
|
+
| `bindHost` | `string` | `'localhost'` | Network interface to bind to. Use `'0.0.0.0'` to expose the server on your local network |
|
|
133
|
+
| `watchFiles` | `boolean` | `true` | Disable chokidar and do a one-time file scan at startup instead |
|
|
134
|
+
| `wsPath` | `string` | `'/hmr'` | WebSocket upgrade path |
|
|
135
|
+
| `watch` | `string[]` | `['src']` | Paths or glob patterns to watch |
|
|
136
|
+
| `ignore` | `string[]` | `[]` | Glob patterns to ignore |
|
|
137
|
+
| `cold` | `string[]` | `[]` | Patterns for files that trigger a full page reload |
|
|
138
|
+
| `extensions` | `string[]` | `.js .cjs .mjs .css` | File extensions to watch |
|
|
139
|
+
| `static` | `string \| false` | `'.'` | Directory to serve static files from. Pass `false` to disable static serving |
|
|
140
|
+
| `indexPath` | `string` | `'index.html'` | Path to `index.html` |
|
|
141
|
+
| `injectLoader` | `string` | | Script path injected into `index.html` before `</head>` |
|
|
142
|
+
| `corsProxy` | `boolean \| string\| CORSProxyConfig` | | Enable the HTTP CORS proxy |
|
|
143
|
+
| `wsProxy` | `WSProxyConfig` | | Proxy WebSocket connections to an upstream server |
|
|
144
|
+
| `filesEndpoint` | `boolean \| string` | `'/files'` | Expose the watched file list as JSON. `true` mounts at `/files` |
|
|
145
|
+
| `configEndpoint` | `boolean \| string` | `'/config'` | Expose the server config as JSON. `false` to disable |
|
|
146
|
+
| `getFiles` | `() => string[]` | | Override the file list sent to connecting clients |
|
|
147
|
+
| `onConnect` | `(client, data) => void` | | Called when an HMR client connects |
|
|
148
|
+
| `onDisconnect` | `(client) => void` | | Called when an HMR client disconnects |
|
|
149
|
+
| `logFiles` | `boolean` | `false` | Log every watched file during startup |
|
|
150
|
+
| `logProxy` | `boolean \| { cors?: boolean, ws?: boolean }` | `false` | Log proxy traffic |
|
|
151
|
+
| `tls` | `TLSConfig` | | Enable HTTPS / WSS |
|
|
152
|
+
| `handleSignals` | `boolean \| string[]` | `true` | Register signal handlers for clean shutdown. false to opt out, or pass an array of signal names |
|
|
153
|
+
|
|
154
|
+
### Methods
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
server.start(): Promise<void>
|
|
158
|
+
server.stop(): Promise<void>
|
|
159
|
+
server.send(client: WebSocket, payload: Object): boolean
|
|
160
|
+
server.broadcast(action: string, file: string, extra?: Object): void
|
|
161
|
+
server.getConfig(): Object
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### CORS Proxy
|
|
167
|
+
|
|
168
|
+
Enabling `corsProxy` mounts an HTTP proxy on the dev server. The browser hits a local URL and the server forwards the request upstream, injecting CORS headers onto the response. This means no browser extensions, no separate proxy process.
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
corsProxy: {
|
|
172
|
+
path: '/proxy', // default, can also be a RegExp
|
|
173
|
+
|
|
174
|
+
// Customize outbound headers per request
|
|
175
|
+
getHeaders: (targetUrl, incomingRequest) => ({
|
|
176
|
+
'Authorization': `Bearer ${getToken()}`,
|
|
177
|
+
'User-Agent': 'Mozilla/5.0',
|
|
178
|
+
'X-Forwarded-For': incomingRequest.headers.get('x-real-ip'),
|
|
179
|
+
}),
|
|
180
|
+
|
|
181
|
+
// Intercept and rewrite the upstream response before it reaches the browser
|
|
182
|
+
transformResponse: async (response) => {
|
|
183
|
+
const json = await response.json();
|
|
184
|
+
return new Response(JSON.stringify(patch(json)), response);
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
// Usage from the browser
|
|
191
|
+
const res = await fetch(
|
|
192
|
+
"http://localhost:1338/proxy/https://api.example.com/data",
|
|
193
|
+
);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
### WebSocket Proxy
|
|
199
|
+
|
|
200
|
+
`wsProxy` tunnels WebSocket connections from the browser through the dev server to an upstream host. Useful for connecting to game servers, remote APIs, or any WS service that would otherwise be blocked by CORS or mixed-content rules.
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
wsProxy: {
|
|
204
|
+
path: '/proxy',
|
|
205
|
+
|
|
206
|
+
// Static headers sent on every upstream connection
|
|
207
|
+
headers: {
|
|
208
|
+
Origin: 'https://www.example.com',
|
|
209
|
+
'User-Agent': 'Mozilla/5.0',
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
// Forward select client headers upstream (or pass `true` to forward all)
|
|
213
|
+
forwardHeaders: ['cookie', 'authorization'],
|
|
214
|
+
|
|
215
|
+
// Dynamic headers per connection
|
|
216
|
+
getHeaders: (targetUrl, clientHeaders) => ({
|
|
217
|
+
'X-Session': resolveSession(clientHeaders['cookie']),
|
|
218
|
+
}),
|
|
219
|
+
|
|
220
|
+
// Intercept messages in either direction
|
|
221
|
+
onClientMessage: (message, clientSocket, upstreamSocket) => {
|
|
222
|
+
const data = JSON.parse(message);
|
|
223
|
+
if (data.type === 'PING') return; // drop client pings
|
|
224
|
+
upstreamSocket.send(message);
|
|
225
|
+
},
|
|
226
|
+
onUpstreamMessage: (message, clientSocket, upstreamSocket) => {
|
|
227
|
+
clientSocket.send(transform(message));
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
onConnect: (targetUrl) => console.log('Proxy connected to', targetUrl),
|
|
231
|
+
|
|
232
|
+
// Extra options forwarded to the upstream WebSocket constructor
|
|
233
|
+
options: { perMessageDeflate: true },
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
// Usage from the browser -- the full upstream URL goes after the path prefix
|
|
239
|
+
const ws = new WebSocket(
|
|
240
|
+
"ws://localhost:1338/proxy/wss://game.example.com:9081/",
|
|
241
|
+
);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
### Static Server and Loader Injection
|
|
247
|
+
|
|
248
|
+
Setting `static` serves a directory over HTTP. Setting `injectLoader` inserts a `<script>` tag for the given file into `index.html` at request time, so you never have to edit the HTML manually.
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
new HMRServer({
|
|
252
|
+
port: 1338,
|
|
253
|
+
watch: ["src"],
|
|
254
|
+
static: ".",
|
|
255
|
+
indexPath: "index.html",
|
|
256
|
+
injectLoader: "src/loader.mjs", // automatically injected before </head>
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`.mjs` loader files are injected with `type="module"`. All static responses include `Cache-Control: no-cache` headers so the browser never serves stale files during development.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### TLS
|
|
265
|
+
|
|
266
|
+
Pass `tls` to switch the server to HTTPS and WSS. The client's `secure` option or a `wss://` URL flips the client to match.
|
|
267
|
+
|
|
268
|
+
```js
|
|
269
|
+
new HMRServer({
|
|
270
|
+
port: 1338,
|
|
271
|
+
watch: ["src"],
|
|
272
|
+
tls: {
|
|
273
|
+
key: "localhost-key.pem",
|
|
274
|
+
cert: "localhost.pem",
|
|
275
|
+
ca: "ca.pem", // optional, for mutual TLS
|
|
276
|
+
passphrase: "secret", // optional, for encrypted keys
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
```js
|
|
282
|
+
new HMRClient({ port: 1338, secure: true });
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
### Local Network Sharing
|
|
288
|
+
|
|
289
|
+
Set `bindHost: '0.0.0.0'` to expose the server on all network interfaces. Any device on the same network can then connect using your machine's local IP with no extra configuration needed. The injected loader URL is derived automatically from the `Host` header of each incoming request, so local devices get `localhost` and remote devices get whatever address they used to reach the server.
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
new HMRServer({
|
|
293
|
+
port: 1338,
|
|
294
|
+
bindHost: "0.0.0.0",
|
|
295
|
+
watch: ["core"],
|
|
296
|
+
injectLoader: "loader.mjs",
|
|
297
|
+
tls: {
|
|
298
|
+
key: "localhost-key.pem",
|
|
299
|
+
cert: "localhost.pem",
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
This also works with domains, if you're running on a VPS with a domain pointed at it, devices anywhere can connect to it.
|
|
305
|
+
|
|
306
|
+
Here is how you can find your local IP that other clients would need to connect to your hmr server:
|
|
307
|
+
|
|
308
|
+
**Mac:**
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
ipconfig getifaddr $(route get default | grep interface | awk '{print $2}')
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Linux:**
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
ip route get 1 | awk '{print $7; exit}'
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Windows:**
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
ipconfig | findstr /i "IPv4"
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
> **Firewall rules**: only needed if your OS blocks incoming connections on your chosen port. Replace `1338` with your actual port.
|
|
327
|
+
>
|
|
328
|
+
> **Windows** (run as admin):
|
|
329
|
+
>
|
|
330
|
+
> ```
|
|
331
|
+
> netsh advfirewall firewall add rule name="Cindel HMR" dir=in action=allow protocol=TCP localport=1338
|
|
332
|
+
> ```
|
|
333
|
+
>
|
|
334
|
+
> **Linux with ufw:**
|
|
335
|
+
>
|
|
336
|
+
> ```bash
|
|
337
|
+
> sudo ufw allow 1338/tcp
|
|
338
|
+
> ```
|
|
339
|
+
>
|
|
340
|
+
> **Linux with firewalld:**
|
|
341
|
+
>
|
|
342
|
+
> ```bash
|
|
343
|
+
> sudo firewall-cmd --add-port=1338/tcp --permanent && sudo firewall-cmd --reload
|
|
344
|
+
> ```
|
|
345
|
+
>
|
|
346
|
+
> Mac does not require a firewall rule, it works out of the box.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
### Signal Handling
|
|
351
|
+
|
|
352
|
+
By default cindel registers `SIGINT` and `SIGTERM` handlers so Ctrl+C and process
|
|
353
|
+
managers like Docker, PM2, and systemd all shut down cleanly without leaving the
|
|
354
|
+
chokidar watcher or Bun server hanging.
|
|
355
|
+
|
|
356
|
+
```js
|
|
357
|
+
// Default: SIGINT + SIGTERM
|
|
358
|
+
new HMRServer({
|
|
359
|
+
port: 1338,
|
|
360
|
+
watch: ["src"],
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Add SIGHUP for terminal-close and Nodemon compat
|
|
364
|
+
new HMRServer({
|
|
365
|
+
port: 1338,
|
|
366
|
+
watch: ["src"],
|
|
367
|
+
handleSignals: ["SIGINT", "SIGTERM", "SIGHUP"],
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Opt out entirely and manage shutdown yourself
|
|
371
|
+
const server = new HMRServer({
|
|
372
|
+
port: 1338,
|
|
373
|
+
watch: ["src"],
|
|
374
|
+
handleSignals: false,
|
|
375
|
+
});
|
|
376
|
+
process.on("SIGINT", () => server.stop().then(() => process.exit(0)));
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Client
|
|
382
|
+
|
|
383
|
+
### `new HMRClient(options)`
|
|
384
|
+
|
|
385
|
+
`options` can be shorthand:
|
|
386
|
+
|
|
387
|
+
- **`number`** treated as `{ port: n }`, connects to `ws://localhost:<n>`
|
|
388
|
+
- **`string`** treated as a full WebSocket URL
|
|
389
|
+
- **`object`** full config, see below
|
|
390
|
+
|
|
391
|
+
| Option | Type | Default | Description |
|
|
392
|
+
| ------------------- | ------------------------------------ | ------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
393
|
+
| `port` | `number` | | Port number |
|
|
394
|
+
| `host` | `string` | `'localhost'` | Hostname |
|
|
395
|
+
| `secure` | `boolean` | `false` | Use `wss://` and `https://` |
|
|
396
|
+
| `wsUrl` | `string` | | Explicit WebSocket URL, overrides host/port |
|
|
397
|
+
| `httpUrl` | `string` | | Explicit HTTP base URL for file fetching |
|
|
398
|
+
| `wsPath` | `string` | `'/hmr'` | WebSocket path |
|
|
399
|
+
| `autoReconnect` | `boolean` | `true` | Reconnect on disconnect with exponential backoff |
|
|
400
|
+
| `reconnectDelay` | `number` | `2000` | Base reconnect delay in ms |
|
|
401
|
+
| `maxReconnectDelay` | `number` | `30000` | Maximum reconnect delay cap in ms |
|
|
402
|
+
| `skip` | `string[]` | | Glob patterns for files to never load |
|
|
403
|
+
| `filterSkip` | `(file, allFiles) => boolean` | | Custom skip logic, OR'd with `skip` |
|
|
404
|
+
| `cold` | `string[]` | | Glob patterns that trigger a full page reload. Merged with the server's `cold` config on connect |
|
|
405
|
+
| `filterCold` | `(file) => boolean` | | Custom cold logic, OR'd with `cold` |
|
|
406
|
+
| `getOverrideTarget` | `(file, allFiles) => string \| null` | | Map an override file to the original it replaces |
|
|
407
|
+
| `onFileLoaded` | `(file) => void` | | Called after each file is loaded or reloaded |
|
|
408
|
+
| `sortFiles` | `(files) => string[]` | CSS before JS, cold first | Custom sort for the initial load order |
|
|
409
|
+
|
|
410
|
+
### Methods
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
client.connect(): Promise<void>
|
|
414
|
+
client.disconnect(): void
|
|
415
|
+
client.on(event, handler): HMRClient // chainable
|
|
416
|
+
client.once(event, handler): HMRClient // chainable
|
|
417
|
+
client.off(event, handler?): HMRClient // chainable
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
### Events
|
|
423
|
+
|
|
424
|
+
Events fire throughout the connection lifecycle and for every file action. All event methods are chainable.
|
|
425
|
+
|
|
426
|
+
```js
|
|
427
|
+
client
|
|
428
|
+
.on("connect", () => {
|
|
429
|
+
console.log("HMR connected");
|
|
430
|
+
})
|
|
431
|
+
.on("disconnect", () => {
|
|
432
|
+
showBanner("Dev server offline, reconnecting...");
|
|
433
|
+
})
|
|
434
|
+
.on("init", ({ files, config }) => {
|
|
435
|
+
console.log(`Loaded ${files.length} files`);
|
|
436
|
+
console.log("Server cold patterns:", config.cold);
|
|
437
|
+
})
|
|
438
|
+
.on("reload", ({ file }) => {
|
|
439
|
+
console.log(`Hot-reloaded: ${file}`);
|
|
440
|
+
applyChanges(file);
|
|
441
|
+
})
|
|
442
|
+
.on("add", ({ file }) => {
|
|
443
|
+
console.log(`New file available: ${file}`);
|
|
444
|
+
})
|
|
445
|
+
.on("remove", ({ file }) => {
|
|
446
|
+
console.log(`File removed: ${file}`);
|
|
447
|
+
cleanupForFile(file);
|
|
448
|
+
})
|
|
449
|
+
.on("cold", (file) => {
|
|
450
|
+
console.log(`Cold file changed: ${file} -> forcing hard reload`);
|
|
451
|
+
window.location.reload();
|
|
452
|
+
})
|
|
453
|
+
.on("error", (err) => {
|
|
454
|
+
console.error("HMR error:", err);
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
| Event | Payload | Description |
|
|
459
|
+
| ------------ | ------------------- | -------------------------------------- |
|
|
460
|
+
| `connect` | | WebSocket connection established |
|
|
461
|
+
| `disconnect` | | WebSocket disconnected |
|
|
462
|
+
| `init` | `{ files, config }` | Server sent the initial file list |
|
|
463
|
+
| `reload` | `{ file }` | A file was changed and hot-reloaded |
|
|
464
|
+
| `add` | `{ file }` | A new file was detected |
|
|
465
|
+
| `remove` | `{ file }` | A file was removed |
|
|
466
|
+
| `cold` | `file: string` | A cold file changed |
|
|
467
|
+
| `error` | `Error` | A connection or message error occurred |
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
### Skip and Cold Filters
|
|
472
|
+
|
|
473
|
+
`skip` prevents files from ever being loaded by the client. `cold` marks files that need a full page reload rather than a hot swap. Both options accept glob patterns, a custom filter function, or both combined via OR logic.
|
|
474
|
+
|
|
475
|
+
> **Note:** Glob patterns are always relative to the project root, not the watched directory.
|
|
476
|
+
|
|
477
|
+
```js
|
|
478
|
+
new HMRClient({
|
|
479
|
+
port: 1338,
|
|
480
|
+
|
|
481
|
+
// Never load files matching these patterns
|
|
482
|
+
skip: ["**/*.test.js", "_*/**"],
|
|
483
|
+
|
|
484
|
+
// Custom skip logic is context aware, it receives the full file list
|
|
485
|
+
filterSkip: (file, allFiles) => {
|
|
486
|
+
return allFiles.includes(file.replace(".override.js", ".js"));
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
// These files can't be hot-swapped, they need a full reload
|
|
490
|
+
cold: ["**/*.cold.js", "src/bootstrap.js"],
|
|
491
|
+
|
|
492
|
+
// Custom cold logic
|
|
493
|
+
filterCold: (file) => file.includes("/vendor/"),
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
### Override Detection
|
|
500
|
+
|
|
501
|
+
Override detection lets you maintain a parallel directory of replacement files that shadow originals without modifying them. When an override changes, the client unloads the original before loading the override.
|
|
502
|
+
|
|
503
|
+
```js
|
|
504
|
+
new HMRClient({
|
|
505
|
+
port: 1338,
|
|
506
|
+
|
|
507
|
+
// x_mypatch/overrides/core/game.js shadows core/game.js
|
|
508
|
+
getOverrideTarget: (file, allFiles) => {
|
|
509
|
+
const match = file.match(/^x_[^/]+\/overrides\/(.+)$/);
|
|
510
|
+
if (!match) return null;
|
|
511
|
+
const original = match[1];
|
|
512
|
+
return allFiles.includes(original) ? original : null;
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
new HMRClient({
|
|
517
|
+
port: 1338,
|
|
518
|
+
|
|
519
|
+
// any file named `override.<original>` shadows the original
|
|
520
|
+
// e.g. override.utils.js -> utils.js
|
|
521
|
+
getOverrideTarget: (file, allFiles) => {
|
|
522
|
+
const name = file.split("/").pop();
|
|
523
|
+
const match = name.match(/^override\.(.+)$/);
|
|
524
|
+
if (!match) return null;
|
|
525
|
+
|
|
526
|
+
const target = file.replace(name, match[1]);
|
|
527
|
+
return allFiles?.includes(target) ? target : null;
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## Exports
|
|
535
|
+
|
|
536
|
+
| Import path | Environment | Description |
|
|
537
|
+
| ------------------------------------- | ----------- | -------------------- |
|
|
538
|
+
| `cindel` or `cindel/server` | Node / Bun | `HMRServer` |
|
|
539
|
+
| `cindel/client` | Browser ESM | `HMRClient` |
|
|
540
|
+
| `https://cdn.jsdelivr.net/npm/cindel` | Browser CDN | Exposes `window.HMR` |
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## License
|
|
545
|
+
|
|
546
|
+
GPL-3.0-or-later (c) [sneazy-ibo](https://github.com/sneazy-ibo)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Handles loading and hot reloading of JavaScript and CSS files via blob URLs. */
|
|
2
|
+
export class FileLoader {
|
|
3
|
+
constructor(httpUrl: any);
|
|
4
|
+
httpUrl: any;
|
|
5
|
+
/**
|
|
6
|
+
* Debounce state per file. Stores { timeout, resolvers[] } so that
|
|
7
|
+
* when a rapid second change clears the first timeout, the first
|
|
8
|
+
* caller's Promise still resolves with the final load result.
|
|
9
|
+
* @type {Map<string, { timeout: number, resolvers: Function[] }>}
|
|
10
|
+
*/
|
|
11
|
+
loadQueue: Map<string, {
|
|
12
|
+
timeout: number;
|
|
13
|
+
resolvers: Function[];
|
|
14
|
+
}>;
|
|
15
|
+
loadFile(path: any): Promise<any>;
|
|
16
|
+
loadCSS(path: any): Promise<any>;
|
|
17
|
+
loadModule(path: any): Promise<any>;
|
|
18
|
+
loadScript(path: any): Promise<any>;
|
|
19
|
+
reloadFile(path: any): Promise<any>;
|
|
20
|
+
_flushReload(path: any): Promise<void>;
|
|
21
|
+
removeFile(path: any): Promise<void>;
|
|
22
|
+
makeUrl(path: any): string;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=file-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-loader.d.ts","sourceRoot":"","sources":["../../src/client/file-loader.js"],"names":[],"mappings":"AAAA,mFAAmF;AACnF;IACE,0BASC;IARC,aAAsB;IACtB;;;;;OAKG;IACH,WAFU,GAAG,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,UAAU,CAAA;KAAE,CAAC,CAEvC;IAG5B,kCAOC;IAKD,iCAoBC;IAKD,oCAgBC;IAED,oCAeC;IAKD,oCAeC;IAED,uCAUC;IAED,qCAcC;IAGD,2BAGC;CACF"}
|