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/dist/index.js ADDED
@@ -0,0 +1,1142 @@
1
+ // src/server/file-watcher.js
2
+ import chokidar from "chokidar";
3
+ import chalk from "chalk";
4
+ import path from "path";
5
+
6
+ // src/shared/utils.js
7
+ import picomatch from "picomatch";
8
+ var matcherCache = /* @__PURE__ */ new Map();
9
+ function matchGlob(file, patterns) {
10
+ return patterns.some((pattern) => {
11
+ if (!matcherCache.has(pattern)) matcherCache.set(pattern, picomatch(pattern));
12
+ return matcherCache.get(pattern)(file);
13
+ });
14
+ }
15
+ function formatTime() {
16
+ return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", {
17
+ hour12: false,
18
+ hour: "2-digit",
19
+ minute: "2-digit",
20
+ second: "2-digit"
21
+ });
22
+ }
23
+ function getBrowserFromUA(userAgent) {
24
+ if (!userAgent) return "Unknown";
25
+ if (userAgent.includes("Chrome/")) return "Chrome";
26
+ if (userAgent.includes("Firefox/")) return "Firefox";
27
+ if (userAgent.includes("Safari/") && !userAgent.includes("Chrome/")) return "Safari";
28
+ if (userAgent.includes("Edge/")) return "Edge";
29
+ if (userAgent.includes("Opera/") || userAgent.includes("OPR/")) return "Opera";
30
+ return "Unknown";
31
+ }
32
+ function getFileName(path3) {
33
+ return path3.split("/").pop();
34
+ }
35
+ function getFilePath(path3) {
36
+ const parts = path3.split("/");
37
+ parts.pop();
38
+ return parts.join("/") || ".";
39
+ }
40
+ function normalizeUrl(url) {
41
+ return url.endsWith("/") ? url : url + "/";
42
+ }
43
+ function normalizeProxyPath(path3, defaultPath) {
44
+ return (path3 || defaultPath).replace(/^(?!\/)/, "/").replace(/\/+$/, "");
45
+ }
46
+ function resolveEndpoint(value, defaultPath) {
47
+ if (!value) return null;
48
+ if (value === true) return defaultPath;
49
+ return value.startsWith("/") ? value : `/${value}`;
50
+ }
51
+ function wsUrlToHttpUrl(wsUrl) {
52
+ const u = new URL(wsUrl);
53
+ return `${u.protocol === "wss:" ? "https" : "http"}://${u.host}/`;
54
+ }
55
+ function httpUrlToWsUrl(httpUrl, wsPath) {
56
+ const u = new URL(httpUrl);
57
+ return `${u.protocol === "https:" ? "wss" : "ws"}://${u.host}${wsPath}`;
58
+ }
59
+ function resolveConnectionUrls(options, wsPath = "/hmr") {
60
+ if (typeof options === "string") {
61
+ return { wsUrl: options, httpUrl: wsUrlToHttpUrl(options) };
62
+ }
63
+ if (typeof options === "number") {
64
+ options = { port: options };
65
+ }
66
+ if (typeof options !== "object" || options === null) {
67
+ throw new Error("Options must be a string, number, or object");
68
+ }
69
+ if (options.wsUrl && options.httpUrl) {
70
+ return { wsUrl: options.wsUrl, httpUrl: normalizeUrl(options.httpUrl) };
71
+ }
72
+ if (options.wsUrl) {
73
+ return { wsUrl: options.wsUrl, httpUrl: wsUrlToHttpUrl(options.wsUrl) };
74
+ }
75
+ if (options.httpUrl) {
76
+ const httpUrl = normalizeUrl(options.httpUrl);
77
+ return { wsUrl: httpUrlToWsUrl(httpUrl, wsPath), httpUrl };
78
+ }
79
+ if (options.port) {
80
+ const host = options.host || "localhost";
81
+ const secure = options.secure || false;
82
+ const wsProtocol = secure ? "wss" : "ws";
83
+ const httpProtocol = secure ? "https" : "http";
84
+ const wsUrl = `${wsProtocol}://${host}:${options.port}${wsPath}`;
85
+ const httpUrl = normalizeUrl(`${httpProtocol}://${host}:${options.port}`);
86
+ return { wsUrl, httpUrl };
87
+ }
88
+ throw new Error("Must provide wsUrl, httpUrl, port, or host+port");
89
+ }
90
+
91
+ // src/shared/constants.js
92
+ var DEFAULT_PORT = 1338;
93
+ var WATCHABLE_EXTENSIONS = [".js", ".cjs", ".mjs", ".css"];
94
+ var DEFAULT_FILES_ENDPOINT = "/files";
95
+ var DEFAULT_CONFIG_ENDPOINT = "/config";
96
+ var DEFAULT_CORS_PROXY_PATH = "/proxy";
97
+ var DEFAULT_WS_PROXY_PATH = "/proxy";
98
+ var WATCHER_CONFIG = {
99
+ persistent: true,
100
+ ignoreInitial: true,
101
+ awaitWriteFinish: {
102
+ stabilityThreshold: 50,
103
+ pollInterval: 10
104
+ },
105
+ usePolling: false,
106
+ alwaysStat: true,
107
+ atomic: false
108
+ };
109
+ var HMR_ACTIONS = {
110
+ RELOAD: "reload",
111
+ ADD: "add",
112
+ REMOVE: "remove",
113
+ INIT: "init"
114
+ };
115
+
116
+ // src/server/file-watcher.js
117
+ var FileWatcher = class {
118
+ constructor(options) {
119
+ this.paths = options.paths || [];
120
+ this.ignorePatterns = options.ignore || [];
121
+ this.extensions = options.extensions || [];
122
+ this.onChange = options.onChange || (() => {
123
+ });
124
+ this.onAdd = options.onAdd || (() => {
125
+ });
126
+ this.onRemove = options.onRemove || (() => {
127
+ });
128
+ this.onAddDir = options.onAddDir || (() => {
129
+ });
130
+ this.onRemoveDir = options.onRemoveDir || (() => {
131
+ });
132
+ this.onReady = options.onReady || (() => {
133
+ });
134
+ this.logFiles = options.logFiles || false;
135
+ this.logger = options.logger;
136
+ this.watcher = null;
137
+ this._watchedFiles = /* @__PURE__ */ new Set();
138
+ this.isInitializing = true;
139
+ this.loggedFiles = /* @__PURE__ */ new Set();
140
+ }
141
+ shouldIgnore(filePath, stats) {
142
+ const normalized = this.normalizePath(filePath);
143
+ if (stats && stats.isDirectory()) {
144
+ return matchGlob(normalized, this.ignorePatterns);
145
+ }
146
+ if (matchGlob(normalized, this.ignorePatterns)) {
147
+ this.logFile(filePath, true, "ignored pattern");
148
+ return true;
149
+ }
150
+ if (stats && stats.isFile() && !this.isWatchableFile(normalized, this.extensions)) {
151
+ this.logFile(filePath, true, "non-watchable extension");
152
+ return true;
153
+ }
154
+ if (stats && stats.isFile()) {
155
+ this.logFile(filePath, false);
156
+ }
157
+ return false;
158
+ }
159
+ normalizePath(filePath) {
160
+ return path.relative(".", filePath).replace(/\\/g, "/");
161
+ }
162
+ isWatchableFile(filePath, extensions) {
163
+ const ext = path.extname(filePath).toLowerCase();
164
+ return extensions.includes(ext);
165
+ }
166
+ logFile(filePath, ignored, reason = "") {
167
+ if (!this.logFiles || !this.isInitializing) return;
168
+ const normalized = this.normalizePath(filePath);
169
+ if (this.loggedFiles.has(normalized)) return;
170
+ this.loggedFiles.add(normalized);
171
+ this.logger.logInitFile(normalized, ignored, reason);
172
+ }
173
+ getWatchedFiles() {
174
+ return Array.from(this._watchedFiles);
175
+ }
176
+ logWatchedDirectories() {
177
+ if (!this.logFiles || !this.watcher) return;
178
+ const watched = this.watcher.getWatched();
179
+ const watchRoots = this.paths.map((p) => this.normalizePath(p));
180
+ const relevantDirs = /* @__PURE__ */ new Set();
181
+ for (const [dir, files] of Object.entries(watched)) {
182
+ const normalized = this.normalizePath(dir);
183
+ const isWithinWatchRoot = watchRoots.some(
184
+ (root) => normalized === root || normalized.startsWith(root + "/")
185
+ );
186
+ if (!isWithinWatchRoot || normalized === ".") continue;
187
+ const hasWatchableFiles = files.some((file) => {
188
+ const fullPath = `${dir}/${file}`.replace(/\\/g, "/");
189
+ const normalizedFile = this.normalizePath(fullPath);
190
+ return this.isWatchableFile(normalizedFile, this.extensions) && !matchGlob(normalizedFile, this.ignorePatterns);
191
+ });
192
+ if (hasWatchableFiles) {
193
+ relevantDirs.add(normalized);
194
+ for (const root of watchRoots) {
195
+ if (normalized.startsWith(root + "/")) {
196
+ const parts = normalized.substring(root.length + 1).split("/");
197
+ let ancestor = root;
198
+ for (const part of parts.slice(0, -1)) {
199
+ ancestor = `${ancestor}/${part}`;
200
+ relevantDirs.add(ancestor);
201
+ }
202
+ break;
203
+ }
204
+ }
205
+ }
206
+ }
207
+ const sortedDirs = Array.from(relevantDirs).sort();
208
+ if (sortedDirs.length === 0) return;
209
+ console.log(chalk.cyan(`
210
+ ${this.logger.symbols.watch} Watching directories:`));
211
+ sortedDirs.forEach((dir) => {
212
+ console.log(chalk.green(` ${this.logger.symbols.success} ${dir}`));
213
+ });
214
+ }
215
+ async start() {
216
+ if (this.watcher) {
217
+ this.logger.warning("Watcher already started");
218
+ return;
219
+ }
220
+ if (this.paths.length === 0) {
221
+ this.logger.warning("No paths to watch");
222
+ return;
223
+ }
224
+ this.logger.watcherStart(this.paths);
225
+ this.isInitializing = true;
226
+ this.watcher = chokidar.watch(this.paths, {
227
+ ...WATCHER_CONFIG,
228
+ ignored: (filePath, stats) => this.shouldIgnore(filePath, stats)
229
+ });
230
+ this.watcher.on("change", (filePath) => {
231
+ const normalized = this.normalizePath(filePath);
232
+ this.onChange(normalized);
233
+ }).on("add", (filePath) => {
234
+ const normalized = this.normalizePath(filePath);
235
+ this._watchedFiles.add(normalized);
236
+ this.onAdd(normalized);
237
+ }).on("unlink", (filePath) => {
238
+ const normalized = this.normalizePath(filePath);
239
+ this._watchedFiles.delete(normalized);
240
+ this.onRemove(normalized);
241
+ }).on("addDir", (dirPath) => {
242
+ const normalized = this.normalizePath(dirPath);
243
+ this.onAddDir(normalized);
244
+ }).on("unlinkDir", (dirPath) => {
245
+ const normalized = this.normalizePath(dirPath);
246
+ this.onRemoveDir(normalized);
247
+ }).on("error", (error) => {
248
+ this.logger.error(`Watcher error: ${error.message}`);
249
+ }).on("ready", () => {
250
+ this.isInitializing = false;
251
+ this.loggedFiles.clear();
252
+ const watched = this.watcher.getWatched();
253
+ const watchRoots = this.paths.map((p) => this.normalizePath(p));
254
+ let actualFileCount = 0;
255
+ let actualDirCount = 0;
256
+ for (const [dir, files] of Object.entries(watched)) {
257
+ const normalized = this.normalizePath(dir);
258
+ const isWithinWatchRoot = watchRoots.some(
259
+ (root) => normalized === root || normalized.startsWith(root + "/")
260
+ );
261
+ if (!isWithinWatchRoot || normalized === ".") continue;
262
+ let hasWatchableFiles = false;
263
+ for (const file of files) {
264
+ const fullPath = `${dir}/${file}`.replace(/\\/g, "/");
265
+ const normalizedFile = this.normalizePath(fullPath);
266
+ if (this.isWatchableFile(normalizedFile, this.extensions) && !matchGlob(normalizedFile, this.ignorePatterns)) {
267
+ this._watchedFiles.add(normalizedFile);
268
+ actualFileCount++;
269
+ hasWatchableFiles = true;
270
+ }
271
+ }
272
+ if (hasWatchableFiles) actualDirCount++;
273
+ }
274
+ this.logWatchedDirectories();
275
+ console.log(chalk.green(`
276
+ ${this.logger.symbols.watch} Chokidar is ready`));
277
+ console.log(chalk.cyan(`${this.logger.symbols.watch} Watching ${actualFileCount} files across ${actualDirCount} ${actualDirCount === 1 ? "directory" : "directories"}
278
+ `));
279
+ if (actualFileCount === 0) {
280
+ this.logger.watcherNoFiles(this.paths, this.extensions);
281
+ }
282
+ this.onReady();
283
+ });
284
+ return new Promise((resolve) => {
285
+ this.watcher.on("ready", resolve);
286
+ });
287
+ }
288
+ async stop() {
289
+ if (this.watcher) {
290
+ await this.watcher.close();
291
+ this.watcher = null;
292
+ this._watchedFiles.clear();
293
+ }
294
+ }
295
+ };
296
+
297
+ // src/server/hmr-server.js
298
+ import chalk3 from "chalk";
299
+
300
+ // src/shared/logger.js
301
+ import chalk2 from "chalk";
302
+ var Logger = class {
303
+ constructor() {
304
+ this.symbols = {
305
+ debug: "\u25C6",
306
+ info: "\u2139",
307
+ success: "\u2713",
308
+ warning: "\u26A0",
309
+ error: "\u2716",
310
+ config: "\u26ED",
311
+ connect: "\u25B6",
312
+ disconnect: "\u2726",
313
+ change: "\u232C",
314
+ add: "\u2295",
315
+ remove: "\u2296",
316
+ inject: "\u2398",
317
+ startup: "\u26B5",
318
+ shutdown: "\u26B6",
319
+ corsProxy: "\u29C9",
320
+ wsProxy: "\u224D",
321
+ watch: "\u26AD",
322
+ dirAdd: "\u2B22",
323
+ dirRemove: "\u2B21",
324
+ glob: "\u2042"
325
+ };
326
+ }
327
+ debug(message) {
328
+ console.log(chalk2.gray(`${this.symbols.debug} [${formatTime()}] ${message}`));
329
+ }
330
+ info(message) {
331
+ console.log(chalk2.cyan(`${this.symbols.info} [${formatTime()}] ${message}`));
332
+ }
333
+ success(message) {
334
+ console.log(chalk2.green(`${this.symbols.success} [${formatTime()}] ${message}`));
335
+ }
336
+ warning(message) {
337
+ console.log(chalk2.yellow(`${this.symbols.warning} [${formatTime()}] ${message}`));
338
+ }
339
+ error(message) {
340
+ console.log(chalk2.red(`${this.symbols.error} [${formatTime()}] ${message}`));
341
+ }
342
+ custom(symbol, message, color = "white") {
343
+ const sym = this.symbols[symbol] || symbol;
344
+ const msg = `${sym} [${formatTime()}] ${message}`;
345
+ console.log(chalk2[color](msg));
346
+ }
347
+ file(symbol, filePath, color = "white", prefix = "") {
348
+ const sym = this.symbols[symbol] || symbol;
349
+ const fileName = getFileName(filePath);
350
+ const dirPath = getFilePath(filePath);
351
+ const prefixText = prefix ? `${prefix}: ` : "";
352
+ const msg = `${sym} [${formatTime()}] ${prefixText}${chalk2.bold(fileName)} ${chalk2.gray.italic(`(${dirPath})`)}`;
353
+ console.log(chalk2[color](msg));
354
+ }
355
+ banner(name, config) {
356
+ console.log(chalk2.bgCyan.black.bold(`${this.symbols.startup} ${name} Starting
357
+ `));
358
+ const httpProtocol = config.tls ? "https" : "http";
359
+ const wsProtocol = config.tls ? "wss" : "ws";
360
+ const lines = [
361
+ [config.httpServer, "blue", `${this.symbols.startup} ${httpProtocol.toUpperCase()} server started on ${httpProtocol}://localhost:${config.port}`],
362
+ [config.websocket, "blue", `${this.symbols.startup} WebSocket HMR on ${wsProtocol}://localhost:${config.port}${config.wsPath}`],
363
+ [config.corsProxy, "cyan", `${this.symbols.corsProxy} CORS proxy available at ${config.corsProxy.path}`],
364
+ [config.wsProxy, "cyan", `${this.symbols.wsProxy} WS proxy available at ${config.wsProxy.path}`],
365
+ [config.injectLoader, "magenta", `${this.symbols.inject} Injecting loader into index.html (${config.loaderPath})`],
366
+ [!config.injectLoader && config.loaderPath, "yellow", `${this.symbols.warning} Loader file not found at "${config.loaderPath}" -> injection disabled`],
367
+ [config.logFiles, "yellow", `${this.symbols.config} File logging enabled`],
368
+ [config.logProxy?.cors, "yellow", `${this.symbols.config} CORS proxy logging enabled`],
369
+ [config.logProxy?.ws, "yellow", `${this.symbols.config} WS proxy logging enabled`]
370
+ ];
371
+ for (const [condition, color, message] of lines) {
372
+ if (condition) console.log(chalk2[color](message));
373
+ }
374
+ const lists = [
375
+ [config.watch, "cyan", `
376
+ ${this.symbols.glob} Watching`],
377
+ [config.ignore, "gray", `${this.symbols.glob} Ignoring`],
378
+ [config.cold, "blue", `${this.symbols.glob} Cold files`]
379
+ ];
380
+ for (const [arr, color, label] of lists) {
381
+ if (arr && arr.length > 0) {
382
+ console.log(chalk2[color](`${label}: ${arr.join(", ")}`));
383
+ }
384
+ }
385
+ }
386
+ // reqBody / resBody are optional short previews (caller truncates)
387
+ corsProxyRequest(method, url, { reqBody, status, statusText, resBody } = {}) {
388
+ const sym = this.symbols.corsProxy;
389
+ const time = formatTime();
390
+ const statusColor = status >= 500 ? "red" : status >= 400 ? "yellow" : "green";
391
+ console.log(chalk2.cyan(`${sym} [${time}] ${method} ${url}`));
392
+ const rows = [];
393
+ if (reqBody) rows.push({ label: "Request body", value: reqBody });
394
+ rows.push({ label: "Response status", value: `${chalk2[statusColor](status)} ${statusText}` });
395
+ if (resBody) rows.push({ label: "Response body", value: resBody });
396
+ rows.forEach((row, i) => {
397
+ const branch = i === rows.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
398
+ console.log(chalk2.gray(` ${branch} ${row.label}: `) + chalk2.white(row.value));
399
+ });
400
+ }
401
+ // listing every sent header on new ws proxy connection
402
+ wsProxyConnect(id, url, headers) {
403
+ const sym = this.symbols.wsProxy;
404
+ const time = formatTime();
405
+ const entries = Object.entries(headers);
406
+ console.log(chalk2.cyan(`${sym} [${time}] [${id}] Connected ${url}`));
407
+ if (entries.length === 0) return;
408
+ entries.forEach(([k, v], i) => {
409
+ const branch = i === entries.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
410
+ console.log(chalk2.gray(` ${branch} `) + chalk2.yellow(k) + chalk2.gray(": ") + chalk2.white(v));
411
+ });
412
+ }
413
+ shutdown() {
414
+ console.log(chalk2.bgRed.white.bold(`
415
+ ${this.symbols.shutdown} Shutting down... `));
416
+ }
417
+ watcherStart(paths) {
418
+ const pathsDisplay = paths.join(", ");
419
+ console.log(chalk2.cyan(`
420
+ ${this.symbols.watch} Starting file watcher for: ${pathsDisplay}`));
421
+ console.log(chalk2.grey(`${this.symbols.watch} Enabling hot directory detection`));
422
+ }
423
+ logInitFile(filePath, ignored, reason = "") {
424
+ if (ignored) {
425
+ const reasonText = reason ? ` (${reason})` : "";
426
+ console.log(chalk2.red(` ${this.symbols.error} ${filePath}${reasonText}`));
427
+ } else {
428
+ console.log(chalk2.green(` ${this.symbols.success} ${filePath}`));
429
+ }
430
+ }
431
+ watcherNoFiles(patterns, extensions) {
432
+ console.log(chalk2.yellow(`
433
+ ${this.symbols.warning} Warning: No files found matching watch patterns!`));
434
+ console.log(chalk2.yellow(` Patterns: ${patterns.join(", ")}`));
435
+ console.log(chalk2.yellow(` Check that:`));
436
+ console.log(chalk2.yellow(` \u2022 Paths exist`));
437
+ console.log(chalk2.yellow(` \u2022 File extensions match: ${extensions.join(", ")}`));
438
+ console.log(chalk2.yellow(` \u2022 Ignore patterns aren't too broad
439
+ `));
440
+ }
441
+ };
442
+
443
+ // src/server/routes.js
444
+ import path2 from "path";
445
+ async function handleRoutes(req, server) {
446
+ const url = new URL(req.url);
447
+ if (server.configEndpoint && url.pathname === server.configEndpoint) {
448
+ return Response.json(server.getConfig());
449
+ }
450
+ if (server.filesEndpoint && url.pathname === server.filesEndpoint) {
451
+ return handleFilesEndpoint(server);
452
+ }
453
+ if (server.corsProxy) {
454
+ const response = await handleCORSProxy(req, url, server);
455
+ if (response) return response;
456
+ }
457
+ if (server.injectLoader && url.pathname === "/") {
458
+ return handleIndexInjection(req, server);
459
+ }
460
+ if (server.staticDir) {
461
+ return handleStaticFile(url, server);
462
+ }
463
+ return new Response("Not Found", { status: 404 });
464
+ }
465
+ function handleFilesEndpoint(server) {
466
+ try {
467
+ const files = server.getFilesCallback ? server.getFilesCallback() : server.watcher.getWatchedFiles();
468
+ return Response.json(files);
469
+ } catch (error) {
470
+ server.logger.error(`Error getting files: ${error.message}`);
471
+ return Response.json([], { status: 500 });
472
+ }
473
+ }
474
+ async function handleCORSProxy(req, url, server) {
475
+ const config = server.corsProxy;
476
+ const urlPath = url.pathname;
477
+ let matches = false;
478
+ if (typeof config.path === "string") {
479
+ matches = urlPath.startsWith(config.path + "/");
480
+ } else if (config.path instanceof RegExp) {
481
+ matches = config.path.test(urlPath);
482
+ }
483
+ if (!matches) return null;
484
+ const targetUrl = typeof config.path === "string" ? urlPath.slice(config.path.length + 1) : urlPath.replace(config.path, "");
485
+ if (!targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) {
486
+ return new Response("Invalid target URL", { status: 400 });
487
+ }
488
+ try {
489
+ const outboundHeaders = config.getHeaders ? config.getHeaders(targetUrl, req) : {
490
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
491
+ "Content-Type": req.headers.get("content-type") || "text/plain"
492
+ };
493
+ let fetchBody;
494
+ let reqBodyPreview;
495
+ if (server.logProxy.cors && !["GET", "HEAD"].includes(req.method)) {
496
+ reqBodyPreview = (await req.text()).slice(0, 200);
497
+ fetchBody = reqBodyPreview;
498
+ } else {
499
+ fetchBody = ["GET", "HEAD"].includes(req.method) ? void 0 : req.body;
500
+ }
501
+ const response = await fetch(targetUrl, {
502
+ method: req.method,
503
+ headers: outboundHeaders,
504
+ body: fetchBody
505
+ });
506
+ const finalResponse = config.transformResponse ? await config.transformResponse(response) : response;
507
+ const responseHeaders = new Headers(finalResponse.headers);
508
+ responseHeaders.set("Access-Control-Allow-Origin", "*");
509
+ responseHeaders.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
510
+ responseHeaders.set("Access-Control-Allow-Headers", "*");
511
+ if (req.method === "OPTIONS") {
512
+ return new Response(null, { status: 204, headers: responseHeaders });
513
+ }
514
+ if (server.logProxy.cors) {
515
+ const resText = await finalResponse.text();
516
+ server.logger.corsProxyRequest(req.method, targetUrl, {
517
+ reqBody: reqBodyPreview,
518
+ status: finalResponse.status,
519
+ statusText: finalResponse.statusText,
520
+ resBody: resText.slice(0, 200)
521
+ });
522
+ return new Response(resText, {
523
+ status: finalResponse.status,
524
+ statusText: finalResponse.statusText,
525
+ headers: responseHeaders
526
+ });
527
+ }
528
+ return new Response(finalResponse.body, {
529
+ status: finalResponse.status,
530
+ statusText: finalResponse.statusText,
531
+ headers: responseHeaders
532
+ });
533
+ } catch (error) {
534
+ server.logger.error(`CORS proxy error: ${error.message}`);
535
+ return new Response("0", { status: 500 });
536
+ }
537
+ }
538
+ async function handleIndexInjection(req, server) {
539
+ try {
540
+ const indexFile = server.indexPath || "index.html";
541
+ if (server.staticDir && path2.isAbsolute(indexFile)) {
542
+ server.logger.error(`indexPath must be relative when staticDir is set, got: "${indexFile}"`);
543
+ return new Response("Server configuration error", { status: 500 });
544
+ }
545
+ const resolved = server.staticDir ? path2.resolve(server.staticDir, indexFile) : path2.resolve(indexFile);
546
+ const file = Bun.file(resolved);
547
+ const html = await file.text();
548
+ if (!server.loaderPath) {
549
+ return new Response(html, {
550
+ headers: { "Content-Type": "text/html" }
551
+ });
552
+ }
553
+ const requestHost = req.headers.get("host");
554
+ const protocol = server.tls ? "https" : "http";
555
+ const loaderUrl = `${protocol}://${requestHost}/` + server.loaderPath.replace(/\\/g, "/").replace(/^\.\//, "");
556
+ const isModule = loaderUrl.endsWith(".mjs");
557
+ const typeAttr = isModule ? ' type="module"' : "";
558
+ const injectedHtml = html.replace(
559
+ "</head>",
560
+ `<script${typeAttr} src="${loaderUrl}"></script></head>`
561
+ );
562
+ return new Response(injectedHtml, {
563
+ headers: { "Content-Type": "text/html" }
564
+ });
565
+ } catch (error) {
566
+ server.logger.error(`Error loading index.html: ${error.message}`);
567
+ return new Response("Error loading index.html", { status: 500 });
568
+ }
569
+ }
570
+ var MIME_TYPES = {
571
+ // HTML, JS, CSS, JSON, source maps
572
+ ".html": "text/html",
573
+ ".js": "application/javascript",
574
+ ".cjs": "application/javascript",
575
+ ".mjs": "application/javascript",
576
+ ".css": "text/css",
577
+ ".json": "application/json",
578
+ ".map": "application/json",
579
+ // Images
580
+ ".png": "image/png",
581
+ ".jpg": "image/jpeg",
582
+ ".jpeg": "image/jpeg",
583
+ ".gif": "image/gif",
584
+ ".svg": "image/svg+xml",
585
+ ".ico": "image/x-icon",
586
+ ".webp": "image/webp",
587
+ ".avif": "image/avif",
588
+ // Fonts
589
+ ".woff": "font/woff",
590
+ ".woff2": "font/woff2",
591
+ ".ttf": "font/ttf",
592
+ ".otf": "font/otf",
593
+ // Video & Audio
594
+ ".mp4": "video/mp4",
595
+ ".webm": "video/webm",
596
+ ".ogg": "audio/ogg",
597
+ ".mp3": "audio/mpeg",
598
+ ".wav": "audio/wav"
599
+ };
600
+ async function handleStaticFile(url, server) {
601
+ let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
602
+ if (url.pathname === "/" && server.indexPath) {
603
+ filePath = "/" + server.indexPath.replace(/^\//, "");
604
+ }
605
+ const baseResolved = path2.resolve(server.staticDir);
606
+ const resolved = path2.resolve(baseResolved, "." + filePath);
607
+ if (!resolved.startsWith(baseResolved + path2.sep)) {
608
+ return new Response("Forbidden", { status: 403 });
609
+ }
610
+ let file = Bun.file(resolved);
611
+ if (!await file.exists()) {
612
+ const indexPath = path2.join(resolved, "index.html");
613
+ file = Bun.file(indexPath);
614
+ if (!await file.exists()) {
615
+ return new Response("Not Found", { status: 404 });
616
+ }
617
+ }
618
+ const ext = path2.extname(file.name).toLowerCase();
619
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
620
+ return new Response(file, {
621
+ headers: {
622
+ "Content-Type": contentType,
623
+ "Cache-Control": "no-cache, no-store, must-revalidate",
624
+ "Pragma": "no-cache",
625
+ "Expires": "0"
626
+ }
627
+ });
628
+ }
629
+
630
+ // src/server/ws-proxy.js
631
+ function handleWSProxy(client, path3, config, logger, logProxy) {
632
+ const targetUrl = path3.slice(config.path.length + 1);
633
+ try {
634
+ const parsed = new URL(targetUrl);
635
+ if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
636
+ throw new Error(`Invalid protocol: ${parsed.protocol}`);
637
+ }
638
+ } catch (e) {
639
+ logger.custom("wsProxy", `Invalid WS proxy target: ${e.message}`, "red");
640
+ client.close(1002, "Invalid target URL");
641
+ return null;
642
+ }
643
+ client.data.isProxy = true;
644
+ const id = Math.random().toString(36).slice(2, 7);
645
+ const tag = `[${id}]`;
646
+ let headers = {};
647
+ if (config.forwardHeaders && client.data.headers) {
648
+ if (config.forwardHeaders === true) {
649
+ headers = { ...client.data.headers };
650
+ } else if (Array.isArray(config.forwardHeaders)) {
651
+ for (const name of config.forwardHeaders) {
652
+ const value = client.data.headers?.[name.toLowerCase()];
653
+ if (value !== void 0) {
654
+ headers[name.toLowerCase()] = value;
655
+ }
656
+ }
657
+ }
658
+ }
659
+ if (config.headers) {
660
+ headers = { ...headers, ...config.headers };
661
+ }
662
+ if (config.getHeaders) {
663
+ headers = { ...headers, ...config.getHeaders(targetUrl, client.data.headers) };
664
+ }
665
+ const wsOptions = { headers };
666
+ if (config.options) {
667
+ Object.assign(wsOptions, config.options);
668
+ }
669
+ let upstream;
670
+ try {
671
+ upstream = new WebSocket(targetUrl, wsOptions);
672
+ } catch (error) {
673
+ logger.custom("wsProxy", `${tag} Failed to create upstream WebSocket: ${error.message}`, "red");
674
+ client.close(1011, "Upstream connection failed");
675
+ return;
676
+ }
677
+ client.data.upstream = upstream;
678
+ let closed = false;
679
+ const closeBoth = () => {
680
+ if (closed) return;
681
+ closed = true;
682
+ clientOpen = false;
683
+ upstreamReady = false;
684
+ try {
685
+ client.close();
686
+ } catch {
687
+ }
688
+ try {
689
+ upstream.close();
690
+ } catch {
691
+ }
692
+ if (logProxy) logger.custom("wsProxy", `${tag} Closed ${targetUrl}`, "gray");
693
+ };
694
+ const pendingClientMessages = [];
695
+ const MAX_PENDING = 512;
696
+ let clientOpen = true;
697
+ let upstreamReady = false;
698
+ const clientMessageHandler = config.onClientMessage || ((message, _client, upstream2) => {
699
+ if (logProxy && typeof message === "string") {
700
+ logger.custom("wsProxy", `${tag} -> ${message.slice(0, 200)}`, "gray");
701
+ }
702
+ if (upstreamReady) {
703
+ upstream2.send(message);
704
+ } else {
705
+ if (pendingClientMessages.length >= MAX_PENDING) {
706
+ pendingClientMessages.shift();
707
+ }
708
+ pendingClientMessages.push(message);
709
+ }
710
+ });
711
+ const upstreamMessageHandler = config.onUpstreamMessage || ((message, client2, _upstream) => {
712
+ if (logProxy && typeof message === "string") {
713
+ logger.custom("wsProxy", `${tag} <- ${message.slice(0, 200)}`, "gray");
714
+ }
715
+ if (clientOpen && client2.readyState === WebSocket.OPEN) {
716
+ try {
717
+ client2.send(message);
718
+ } catch {
719
+ }
720
+ }
721
+ });
722
+ upstream.onmessage = (event) => {
723
+ upstreamMessageHandler(event.data, client, upstream);
724
+ };
725
+ upstream.onclose = closeBoth;
726
+ upstream.onerror = (event) => {
727
+ const message = event instanceof ErrorEvent ? event.message : "Connection failed";
728
+ if (logProxy) logger.custom("wsProxy", `${tag} Upstream error: ${message}`, "red");
729
+ closeBoth();
730
+ };
731
+ upstream.onopen = () => {
732
+ upstreamReady = true;
733
+ if (logProxy) logger.wsProxyConnect(id, targetUrl, headers);
734
+ for (const msg of pendingClientMessages) {
735
+ upstream.send(msg);
736
+ }
737
+ pendingClientMessages.length = 0;
738
+ if (config.onConnect) {
739
+ config.onConnect(targetUrl);
740
+ }
741
+ };
742
+ return {
743
+ onMessage: (message) => {
744
+ clientMessageHandler(message, client, upstream);
745
+ },
746
+ onClose: closeBoth
747
+ };
748
+ }
749
+
750
+ // src/server/hmr-server.js
751
+ var HMRServer = class {
752
+ /**
753
+ * @param {Object} [options={}]
754
+ * @param {number} [options.port=1338] - Port to listen on.
755
+ * @param {string} [options.bindHost='localhost'] - Network interface to bind to. Use `'0.0.0.0'` to listen on all interfaces and expose the server on your local network.
756
+ * @param {boolean} [options.watchFiles=true] - Use chokidar to watch files. Set to `false` to scan once at startup for initial file loading only.
757
+ * @param {string} [options.wsPath='/hmr'] - WebSocket upgrade path. Clients must connect to this path.
758
+ * @param {string[]} [options.watch=[]] - Paths or glob patterns to watch (e.g. `['src', 'lib']`)
759
+ * @param {string[]} [options.ignore] - Glob patterns to ignore.
760
+ * @param {string[]} [options.cold] - Glob patterns for files that require a full page reload instead of HMR (e.g. `['**\/*.config.js']`)
761
+ * @param {string[]} [options.extensions] - File extensions to watch. Defaults to `.js .cjs .mjs .css`
762
+ * @param {function(WebSocket, {files: string[], config: Object}): void} [options.onConnect] - Called when an HMR client connects. Defaults to sending an `init` message with the file list.
763
+ * @param {function(WebSocket): void} [options.onDisconnect] - Called when an HMR client disconnects
764
+ * @param {boolean} [options.logFiles=false] - Log every watched file during watcher initialization
765
+ * @param {boolean|{cors?: boolean, ws?: boolean}} [options.logProxy=false] - Log proxy traffic.
766
+ * `true` enables both. Pass `{ cors: true, ws: false }` to enable only one.
767
+ * @param {string} [options.static] - Directory to serve static files from (e.g. `'.'` or `'public'`), Defaults to `'.'` for serving from project root.
768
+ * @param {string} [options.indexPath='index.html'] - Path to index.html, used as the `/` fallback and for loader injection
769
+ * @param {string} [options.injectLoader] - Path to a script that will be injected into index.html via `<script>` before `</head>`
770
+ * @param {boolean|string|CORSProxyConfig} [options.corsProxy] - Enable the HTTP CORS proxy. `true` mounts at `/proxy`. A string uses that as the path directly e.g. `'/cors'`.
771
+ * @param {WSProxyConfig} [options.wsProxy] - Proxy WebSocket connections to an upstream server
772
+ * @param {function(): string[]} [options.getFiles] - Override the file list sent to connecting clients. Called on every new connection.
773
+ * @param {boolean|string} [options.filesEndpoint] - Expose the watched file list as JSON. `true` mounts at `/files`, a string uses that as the path.
774
+ * @param {boolean|string} [options.configEndpoint] - Expose the server config as JSON. `true` mounts at `/config`, a string uses that as the path.
775
+ * @param {TLSConfig} [options.tls] - Enable HTTPS/WSS
776
+ * @param {boolean|string[]} [options.handleSignals=true] - Register signal handlers that call `stop()` and exit cleanly.
777
+ * Default `yes` adds `SIGINT`/`SIGTERM`; `false` disables; or pass an array (e.g. `['SIGINT','SIGTERM','SIGHUP']`).
778
+ */
779
+ constructor(options = {}) {
780
+ this.port = options.port || DEFAULT_PORT;
781
+ this.bindHost = options.bindHost ?? "localhost";
782
+ this.wsPath = options.wsPath || "/hmr";
783
+ this.watchFiles = options.watchFiles ?? true;
784
+ this.watchPaths = options.watch || ["src"];
785
+ this.ignorePaths = options.ignore || [];
786
+ this.coldPatterns = options.cold || [];
787
+ this.extensions = (options.extensions || WATCHABLE_EXTENSIONS).map((e) => e.toLowerCase());
788
+ this.onConnectCallback = options.onConnect || this.defaultOnConnect.bind(this);
789
+ this.onDisconnectCallback = options.onDisconnect || (() => {
790
+ });
791
+ this.logFiles = options.logFiles || false;
792
+ const lp = options.logProxy;
793
+ if (lp === true) {
794
+ this.logProxy = { cors: true, ws: true };
795
+ } else if (lp && typeof lp === "object") {
796
+ this.logProxy = { cors: !!lp.cors, ws: !!lp.ws };
797
+ } else {
798
+ this.logProxy = { cors: false, ws: false };
799
+ }
800
+ this.logger = new Logger();
801
+ this.watcher = null;
802
+ this.server = null;
803
+ this.clients = /* @__PURE__ */ new Set();
804
+ this.staticDir = options.static === false ? null : options.static ?? ".";
805
+ this.getFilesCallback = options.getFiles || null;
806
+ this.filesEndpoint = resolveEndpoint(options.filesEndpoint, DEFAULT_FILES_ENDPOINT);
807
+ this.configEndpoint = resolveEndpoint(options.configEndpoint, DEFAULT_CONFIG_ENDPOINT);
808
+ const proxyConfig = options.corsProxy;
809
+ if (proxyConfig === true) {
810
+ this.corsProxy = { path: DEFAULT_CORS_PROXY_PATH };
811
+ } else if (typeof proxyConfig === "string") {
812
+ this.corsProxy = { path: normalizeProxyPath(proxyConfig, DEFAULT_CORS_PROXY_PATH) };
813
+ } else if (proxyConfig && typeof proxyConfig === "object") {
814
+ const { path: proxyPath, getHeaders, transformResponse, ...rest } = proxyConfig;
815
+ if (Object.keys(rest).length > 0) {
816
+ this.logger.warning(`corsProxy received unknown options: ${Object.keys(rest).join(", ")}, these will be ignored`);
817
+ }
818
+ this.corsProxy = {
819
+ path: normalizeProxyPath(proxyPath, DEFAULT_CORS_PROXY_PATH),
820
+ getHeaders: getHeaders || null,
821
+ transformResponse: transformResponse || null
822
+ };
823
+ } else {
824
+ this.corsProxy = null;
825
+ }
826
+ if (options.wsProxy) {
827
+ this.wsProxy = {
828
+ ...options.wsProxy,
829
+ path: normalizeProxyPath(options.wsProxy.path, DEFAULT_WS_PROXY_PATH)
830
+ };
831
+ } else {
832
+ this.wsProxy = null;
833
+ }
834
+ if (options.injectLoader !== void 0 && options.injectLoader !== null) {
835
+ if (typeof options.injectLoader !== "string" || options.injectLoader.trim().length === 0) {
836
+ throw new Error("injectLoader must be a non-empty string path");
837
+ }
838
+ this.loaderPath = options.injectLoader.trim();
839
+ } else {
840
+ this.loaderPath = null;
841
+ }
842
+ this.injectLoader = this.loaderPath !== null;
843
+ this.indexPath = options.indexPath || "index.html";
844
+ this.tls = options.tls || null;
845
+ const DEFAULT_SIGNALS = ["SIGINT", "SIGTERM"];
846
+ this.handleSignals = options.handleSignals === false ? false : Array.isArray(options.handleSignals) ? options.handleSignals : DEFAULT_SIGNALS;
847
+ }
848
+ isColdFile(file) {
849
+ if (this.coldPatterns.length === 0) return false;
850
+ return matchGlob(file, this.coldPatterns);
851
+ }
852
+ /**
853
+ * Send a message to a single HMR client
854
+ * @param {WebSocket} client - The client to send to
855
+ * @param {Object} payload - The payload to send, will be JSON serialized
856
+ * @returns {boolean} Whether the message was sent successfully
857
+ */
858
+ send(client, payload) {
859
+ try {
860
+ client.send(JSON.stringify(payload));
861
+ return true;
862
+ } catch (error) {
863
+ this.logger.error(`Error sending to client: ${error.message}`);
864
+ this.handleHMRDisconnect(client);
865
+ return false;
866
+ }
867
+ }
868
+ defaultOnConnect(ws, data) {
869
+ this.send(ws, {
870
+ type: HMR_ACTIONS.INIT,
871
+ files: data.files,
872
+ config: data.config
873
+ });
874
+ }
875
+ getConfig() {
876
+ return {
877
+ port: this.port,
878
+ bindHost: this.bindHost,
879
+ wsPath: this.wsPath,
880
+ watch: this.watchPaths,
881
+ ignore: this.ignorePaths,
882
+ cold: this.coldPatterns,
883
+ watchFiles: this.watchFiles,
884
+ extensions: this.extensions,
885
+ static: this.staticDir,
886
+ corsProxy: this.corsProxy,
887
+ wsProxy: this.wsProxy,
888
+ wsProxyHeaders: this.wsProxy?.headers || null,
889
+ wsProxyPrefix: this.wsProxy?.path || null,
890
+ filesEndpoint: this.filesEndpoint,
891
+ injectLoader: this.injectLoader,
892
+ loaderPath: this.loaderPath,
893
+ indexPath: this.indexPath,
894
+ tls: this.tls,
895
+ handleSignals: this.handleSignals,
896
+ logProxy: this.logProxy,
897
+ logFiles: this.logFiles
898
+ };
899
+ }
900
+ async setupWatcher() {
901
+ if (!this.watchFiles) {
902
+ console.log(chalk3.yellow(`
903
+ ${this.logger.symbols.config} WatchFiles disabled - globbing: ${this.watchPaths.join(", ")}`));
904
+ const files = [];
905
+ const extPatterns = this.extensions.map((ext) => `**/*${ext}`);
906
+ for (const pattern of this.watchPaths) {
907
+ const globPattern = pattern.endsWith("*") ? pattern : `${pattern}/**/*`;
908
+ const glob = new Bun.Glob(globPattern);
909
+ for await (const file of glob.scan({ onlyFiles: true })) {
910
+ if (matchGlob(file, extPatterns) && !matchGlob(file, this.ignorePaths)) {
911
+ files.push(file);
912
+ }
913
+ }
914
+ }
915
+ console.log(chalk3.cyan(`${this.logger.symbols.watch} Found ${files.length} file${files.length !== 1 ? "s" : ""} (static snapshot, no watcher running)
916
+ `));
917
+ this.staticFiles = files;
918
+ this.getFilesCallback = () => this.staticFiles;
919
+ return;
920
+ }
921
+ const watcherOptions = {
922
+ logger: this.logger,
923
+ paths: this.watchPaths,
924
+ ignore: this.ignorePaths,
925
+ logFiles: this.logFiles,
926
+ onChange: (path3) => {
927
+ if (this.isColdFile(path3)) {
928
+ this.logger.file("change", path3, "cyanBright", "Cold file");
929
+ this.broadcast(HMR_ACTIONS.RELOAD, path3, { cold: true });
930
+ return;
931
+ }
932
+ this.logger.file("change", path3, "yellow", "File changed");
933
+ this.broadcast(HMR_ACTIONS.RELOAD, path3);
934
+ },
935
+ onAdd: (path3) => {
936
+ this.logger.file("add", path3, "greenBright", "File added");
937
+ this.broadcast(HMR_ACTIONS.ADD, path3);
938
+ },
939
+ onRemove: (path3) => {
940
+ this.logger.file("remove", path3, "red", "File removed");
941
+ this.broadcast(HMR_ACTIONS.REMOVE, path3);
942
+ },
943
+ onAddDir: (path3) => {
944
+ this.logger.file("dirAdd", path3, "cyan", "Directory added");
945
+ },
946
+ onRemoveDir: (path3) => {
947
+ this.logger.file("dirRemove", path3, "red", "Directory removed");
948
+ },
949
+ onReady: () => {
950
+ }
951
+ };
952
+ if (this.extensions) {
953
+ watcherOptions.extensions = this.extensions;
954
+ }
955
+ this.watcher = new FileWatcher(watcherOptions);
956
+ await this.watcher.start();
957
+ }
958
+ broadcast(action, file, extra = {}) {
959
+ const message = JSON.stringify({ action, file, ...extra });
960
+ const dead = [];
961
+ const sentCount = this.clients.size;
962
+ for (const client of this.clients) {
963
+ try {
964
+ client.send(message);
965
+ } catch (error) {
966
+ this.logger.error(`Error sending to client: ${error.message}`);
967
+ dead.push(client);
968
+ }
969
+ }
970
+ for (const client of dead) {
971
+ this.handleHMRDisconnect(client);
972
+ }
973
+ if (sentCount > 1) {
974
+ console.log(chalk3.gray(` \u2514\u2500 Broadcasted to ${sentCount} client${sentCount !== 1 ? "s" : ""}`));
975
+ }
976
+ }
977
+ handleHMRConnection(client) {
978
+ this.clients.add(client);
979
+ const browser = getBrowserFromUA(client.data?.headers?.["user-agent"]);
980
+ this.logger.custom(
981
+ "connect",
982
+ `HMR client connected (${this.clients.size} total) - ${browser}`,
983
+ "green"
984
+ );
985
+ const files = this.getFilesCallback ? this.getFilesCallback() : this.watcher ? this.watcher.getWatchedFiles() : [];
986
+ const data = {
987
+ files,
988
+ config: this.getConfig()
989
+ };
990
+ this.onConnectCallback(client, data);
991
+ }
992
+ handleHMRDisconnect(client) {
993
+ if (!this.clients.has(client)) return;
994
+ this.clients.delete(client);
995
+ this.logger.custom(
996
+ "disconnect",
997
+ `HMR client disconnected (${this.clients.size} remaining)`,
998
+ "red"
999
+ );
1000
+ this.onDisconnectCallback(client);
1001
+ }
1002
+ getWebSocketConfig() {
1003
+ return {
1004
+ open: (client) => {
1005
+ const path3 = client.data?.path || "/";
1006
+ if (this.wsProxy && path3.startsWith(this.wsProxy.path + "/")) {
1007
+ const proxyHandlers = handleWSProxy(client, path3, this.wsProxy, this.logger, this.logProxy.ws);
1008
+ client.data.proxyHandlers = proxyHandlers;
1009
+ return;
1010
+ }
1011
+ this.handleHMRConnection(client);
1012
+ },
1013
+ message: (client, message) => {
1014
+ if (client.data?.isProxy) {
1015
+ client.data.proxyHandlers?.onMessage(message);
1016
+ return;
1017
+ }
1018
+ this.logger.debug(`Received HMR message: ${message}`);
1019
+ },
1020
+ close: (client) => {
1021
+ if (client.data?.isProxy) {
1022
+ client.data.proxyHandlers?.onClose();
1023
+ return;
1024
+ }
1025
+ this.handleHMRDisconnect(client);
1026
+ }
1027
+ };
1028
+ }
1029
+ /**
1030
+ * Start the HMR server
1031
+ * @returns {Promise<void>}
1032
+ */
1033
+ async start() {
1034
+ if (this.loaderPath) {
1035
+ const loaderFile = Bun.file(this.loaderPath);
1036
+ if (!await loaderFile.exists()) {
1037
+ this.injectLoader = false;
1038
+ }
1039
+ }
1040
+ let tlsConfig = null;
1041
+ if (this.tls) {
1042
+ try {
1043
+ tlsConfig = {
1044
+ key: await Bun.file(this.tls.key).text(),
1045
+ cert: await Bun.file(this.tls.cert).text()
1046
+ };
1047
+ if (this.tls.ca) {
1048
+ tlsConfig.ca = await Bun.file(this.tls.ca).text();
1049
+ }
1050
+ if (this.tls.passphrase) {
1051
+ tlsConfig.passphrase = this.tls.passphrase;
1052
+ }
1053
+ } catch (error) {
1054
+ this.logger.error(`Failed to load TLS files: ${error.message}`);
1055
+ throw error;
1056
+ }
1057
+ }
1058
+ const httpProtocol = tlsConfig ? "https" : "http";
1059
+ const wsProtocol = tlsConfig ? "wss" : "ws";
1060
+ this.logger.banner("HMR Server", {
1061
+ ...this.getConfig(),
1062
+ httpServer: !!(this.staticDir || this.corsProxy),
1063
+ websocket: true,
1064
+ protocol: httpProtocol,
1065
+ wsProtocol
1066
+ });
1067
+ await this.setupWatcher();
1068
+ const serverConfig = {
1069
+ port: this.port,
1070
+ hostname: this.bindHost,
1071
+ fetch: async (req, server) => {
1072
+ const url = new URL(req.url);
1073
+ const isHMRPath = url.pathname === this.wsPath;
1074
+ const isWSProxyPath = this.wsProxy && url.pathname.startsWith(this.wsProxy.path + "/");
1075
+ if (isHMRPath || isWSProxyPath) {
1076
+ const upgraded = server.upgrade(req, {
1077
+ data: {
1078
+ path: url.pathname,
1079
+ headers: Object.fromEntries(req.headers.entries())
1080
+ }
1081
+ });
1082
+ if (upgraded) return;
1083
+ }
1084
+ const response = await handleRoutes(req, this);
1085
+ const headers = new Headers(response.headers);
1086
+ headers.set("Access-Control-Allow-Origin", "*");
1087
+ headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
1088
+ headers.set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
1089
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
1090
+ },
1091
+ websocket: this.getWebSocketConfig()
1092
+ };
1093
+ if (tlsConfig) {
1094
+ serverConfig.tls = tlsConfig;
1095
+ }
1096
+ this.server = Bun.serve(serverConfig);
1097
+ if (this.handleSignals) {
1098
+ const shutdown = () => this.stop().then(() => process.exit(0));
1099
+ for (const signal of this.handleSignals) {
1100
+ process.on(signal, shutdown);
1101
+ }
1102
+ }
1103
+ }
1104
+ /**
1105
+ * Stop the HMR server and clean up resources
1106
+ * @returns {Promise<void>}
1107
+ */
1108
+ async stop() {
1109
+ this.logger.shutdown();
1110
+ if (this.server) {
1111
+ this.server.stop(true);
1112
+ }
1113
+ if (this.watcher) {
1114
+ await this.watcher.stop();
1115
+ }
1116
+ this.clients.clear();
1117
+ }
1118
+ };
1119
+ export {
1120
+ DEFAULT_CONFIG_ENDPOINT,
1121
+ DEFAULT_CORS_PROXY_PATH,
1122
+ DEFAULT_FILES_ENDPOINT,
1123
+ DEFAULT_PORT,
1124
+ DEFAULT_WS_PROXY_PATH,
1125
+ FileWatcher,
1126
+ HMRServer,
1127
+ HMR_ACTIONS,
1128
+ Logger,
1129
+ WATCHABLE_EXTENSIONS,
1130
+ WATCHER_CONFIG,
1131
+ HMRServer as default,
1132
+ formatTime,
1133
+ getBrowserFromUA,
1134
+ getFileName,
1135
+ getFilePath,
1136
+ matchGlob,
1137
+ normalizeProxyPath,
1138
+ normalizeUrl,
1139
+ resolveConnectionUrls,
1140
+ resolveEndpoint
1141
+ };
1142
+ //# sourceMappingURL=index.js.map