react-client 1.0.13 ā 1.0.15
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/cli/commands/dev.js +166 -154
- package/dist/server/broadcastManager.js +59 -0
- package/package.json +1 -1
package/dist/cli/commands/dev.js
CHANGED
|
@@ -7,24 +7,24 @@ exports.default = dev;
|
|
|
7
7
|
const esbuild_1 = __importDefault(require("esbuild"));
|
|
8
8
|
const connect_1 = __importDefault(require("connect"));
|
|
9
9
|
const http_1 = __importDefault(require("http"));
|
|
10
|
-
const ws_1 = require("ws");
|
|
11
10
|
const chokidar_1 = __importDefault(require("chokidar"));
|
|
12
11
|
const detect_port_1 = __importDefault(require("detect-port"));
|
|
13
12
|
const prompts_1 = __importDefault(require("prompts"));
|
|
14
13
|
const path_1 = __importDefault(require("path"));
|
|
15
14
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
16
|
-
const loadConfig_1 = require("../../utils/loadConfig");
|
|
17
15
|
const open_1 = __importDefault(require("open"));
|
|
18
16
|
const child_process_1 = require("child_process");
|
|
19
17
|
const chalk_1 = __importDefault(require("chalk"));
|
|
18
|
+
const loadConfig_1 = require("../../utils/loadConfig");
|
|
19
|
+
const broadcastManager_1 = require("../../server/broadcastManager");
|
|
20
20
|
async function dev() {
|
|
21
21
|
const root = process.cwd();
|
|
22
|
-
// š§© Load user config
|
|
23
22
|
const userConfig = await (0, loadConfig_1.loadReactClientConfig)(root);
|
|
24
23
|
const appRoot = path_1.default.resolve(root, userConfig.root || '.');
|
|
25
24
|
const defaultPort = userConfig.server?.port || 5173;
|
|
26
|
-
const
|
|
27
|
-
|
|
25
|
+
const cacheDir = path_1.default.join(appRoot, '.react-client', 'deps');
|
|
26
|
+
await fs_extra_1.default.ensureDir(cacheDir);
|
|
27
|
+
// Detect entry dynamically (main.tsx or main.jsx)
|
|
28
28
|
const possibleEntries = ['src/main.tsx', 'src/main.jsx'];
|
|
29
29
|
const entry = possibleEntries.map((p) => path_1.default.join(appRoot, p)).find((p) => fs_extra_1.default.existsSync(p));
|
|
30
30
|
if (!entry) {
|
|
@@ -32,8 +32,7 @@ async function dev() {
|
|
|
32
32
|
process.exit(1);
|
|
33
33
|
}
|
|
34
34
|
const indexHtml = path_1.default.join(appRoot, 'index.html');
|
|
35
|
-
|
|
36
|
-
// āļø Detect open port
|
|
35
|
+
// Detect open port
|
|
37
36
|
const availablePort = await (0, detect_port_1.default)(defaultPort);
|
|
38
37
|
const port = availablePort;
|
|
39
38
|
if (availablePort !== defaultPort) {
|
|
@@ -48,117 +47,138 @@ async function dev() {
|
|
|
48
47
|
process.exit(0);
|
|
49
48
|
}
|
|
50
49
|
}
|
|
51
|
-
// ā”
|
|
50
|
+
// ā” React-refresh runtime auto install
|
|
52
51
|
function safeResolveReactRefresh() {
|
|
53
52
|
try {
|
|
54
53
|
return require.resolve('react-refresh/runtime');
|
|
55
54
|
}
|
|
56
55
|
catch {
|
|
57
|
-
console.warn(chalk_1.default.yellow('ā ļø react-refresh not found ā
|
|
56
|
+
console.warn(chalk_1.default.yellow('ā ļø react-refresh not found ā installing...'));
|
|
57
|
+
(0, child_process_1.execSync)('npm install react-refresh --no-audit --no-fund --silent', {
|
|
58
|
+
cwd: root,
|
|
59
|
+
stdio: 'inherit',
|
|
60
|
+
});
|
|
61
|
+
return require.resolve('react-refresh/runtime');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
65
|
+
const _reactRefreshRuntime = safeResolveReactRefresh();
|
|
66
|
+
// --- Plugins (core + user)
|
|
67
|
+
const corePlugins = [
|
|
68
|
+
{
|
|
69
|
+
name: 'css-hmr',
|
|
70
|
+
async onTransform(code, id) {
|
|
71
|
+
if (id.endsWith('.css')) {
|
|
72
|
+
const escaped = JSON.stringify(code);
|
|
73
|
+
return `
|
|
74
|
+
const css = ${escaped};
|
|
75
|
+
const style = document.createElement('style');
|
|
76
|
+
style.textContent = css;
|
|
77
|
+
document.head.appendChild(style);
|
|
78
|
+
import.meta.hot && import.meta.hot.accept();
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
return code;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
|
|
86
|
+
const plugins = [...corePlugins, ...userPlugins];
|
|
87
|
+
// š§± Connect app
|
|
88
|
+
const app = (0, connect_1.default)();
|
|
89
|
+
const transformCache = new Map();
|
|
90
|
+
// --- Prebundle persistent deps
|
|
91
|
+
async function prebundleDeps() {
|
|
92
|
+
const pkgFile = path_1.default.join(appRoot, 'package.json');
|
|
93
|
+
if (!fs_extra_1.default.existsSync(pkgFile))
|
|
94
|
+
return;
|
|
95
|
+
const pkg = JSON.parse(await fs_extra_1.default.readFile(pkgFile, 'utf8'));
|
|
96
|
+
const deps = Object.keys(pkg.dependencies || {});
|
|
97
|
+
if (!deps.length)
|
|
98
|
+
return;
|
|
99
|
+
const cached = await fs_extra_1.default.readdir(cacheDir);
|
|
100
|
+
const missing = deps.filter((d) => !cached.includes(d + '.js'));
|
|
101
|
+
if (!missing.length)
|
|
102
|
+
return;
|
|
103
|
+
console.log(chalk_1.default.cyan('š¦ Prebundling:'), missing.join(', '));
|
|
104
|
+
for (const dep of missing) {
|
|
58
105
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
106
|
+
const entryPath = require.resolve(dep, { paths: [appRoot] });
|
|
107
|
+
const outFile = path_1.default.join(cacheDir, dep + '.js');
|
|
108
|
+
await esbuild_1.default.build({
|
|
109
|
+
entryPoints: [entryPath],
|
|
110
|
+
bundle: true,
|
|
111
|
+
platform: 'browser',
|
|
112
|
+
format: 'esm',
|
|
113
|
+
outfile: outFile,
|
|
114
|
+
write: true,
|
|
115
|
+
target: 'es2020',
|
|
62
116
|
});
|
|
63
|
-
console.log(chalk_1.default.green(
|
|
64
|
-
return require.resolve('react-refresh/runtime');
|
|
117
|
+
console.log(chalk_1.default.green(`ā
Cached ${dep}`));
|
|
65
118
|
}
|
|
66
119
|
catch (err) {
|
|
67
120
|
const msg = err instanceof Error ? err.message : String(err);
|
|
68
|
-
console.
|
|
69
|
-
console.error(chalk_1.default.red('ā Failed to install react-refresh automatically.'));
|
|
70
|
-
console.error('Please run: npm install react-refresh');
|
|
71
|
-
process.exit(1);
|
|
121
|
+
console.warn(chalk_1.default.yellow(`ā ļø Skipped ${dep}: ${msg}`));
|
|
72
122
|
}
|
|
73
123
|
}
|
|
74
124
|
}
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
const ctx = await esbuild_1.default.context({
|
|
78
|
-
entryPoints: [entry],
|
|
79
|
-
bundle: true,
|
|
80
|
-
sourcemap: true,
|
|
81
|
-
outdir: outDir,
|
|
82
|
-
define: { 'process.env.NODE_ENV': '"development"' },
|
|
83
|
-
loader: { '.ts': 'ts', '.tsx': 'tsx', '.js': 'jsx', '.jsx': 'jsx' },
|
|
84
|
-
entryNames: '[name]',
|
|
85
|
-
assetNames: 'assets/[name]',
|
|
86
|
-
});
|
|
87
|
-
await ctx.watch();
|
|
88
|
-
console.log(chalk_1.default.gray('š¦ Watching and building dev bundle...'));
|
|
89
|
-
console.log(chalk_1.default.gray(' Output dir:'), chalk_1.default.blue(outDir));
|
|
90
|
-
console.log(chalk_1.default.gray(' Entry file:'), chalk_1.default.yellow(entry));
|
|
91
|
-
// š Connect server setup
|
|
92
|
-
const app = (0, connect_1.default)();
|
|
93
|
-
// š” Security headers
|
|
94
|
-
app.use((_req, res, next) => {
|
|
95
|
-
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
|
96
|
-
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
|
|
97
|
-
next();
|
|
98
|
-
});
|
|
99
|
-
// š§ In-memory cache for /@modules
|
|
100
|
-
const moduleCache = new Map();
|
|
101
|
-
// 1ļøā£ Serve react-refresh runtime with safe browser shim
|
|
102
|
-
app.use('/@react-refresh', async (_req, res) => {
|
|
103
|
-
const runtime = await fs_extra_1.default.readFile(reactRefreshRuntime, 'utf8');
|
|
104
|
-
const shim = `
|
|
105
|
-
window.process = window.process || { env: { NODE_ENV: 'development' } };
|
|
106
|
-
window.module = { exports: {} };
|
|
107
|
-
window.global = window;
|
|
108
|
-
window.require = () => window.module.exports;
|
|
109
|
-
`;
|
|
110
|
-
res.setHeader('Content-Type', 'application/javascript');
|
|
111
|
-
res.end(shim + '\n' + runtime);
|
|
112
|
-
});
|
|
113
|
-
// 2ļøā£ Bare module resolver with memory cache
|
|
125
|
+
await prebundleDeps();
|
|
126
|
+
// --- Serve prebundled modules
|
|
114
127
|
app.use('/@modules/', async (req, res, next) => {
|
|
115
|
-
|
|
128
|
+
const id = req.url?.replace(/^\/@modules\//, '');
|
|
116
129
|
if (!id)
|
|
117
130
|
return next();
|
|
118
|
-
// š§© Normalize: remove leading slashes that may appear (e.g. "/react")
|
|
119
|
-
id = id.replace(/^\/+/, '');
|
|
120
|
-
if (!id)
|
|
121
|
-
return next();
|
|
122
|
-
if (moduleCache.has(id)) {
|
|
123
|
-
res.setHeader('Content-Type', 'application/javascript');
|
|
124
|
-
res.end(moduleCache.get(id));
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
131
|
try {
|
|
132
|
+
const cacheFile = path_1.default.join(cacheDir, id.replace(/\//g, '_') + '.js');
|
|
133
|
+
if (await fs_extra_1.default.pathExists(cacheFile)) {
|
|
134
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
135
|
+
return res.end(await fs_extra_1.default.readFile(cacheFile));
|
|
136
|
+
}
|
|
128
137
|
const entryPath = require.resolve(id, { paths: [appRoot] });
|
|
129
|
-
const
|
|
138
|
+
const result = await esbuild_1.default.build({
|
|
130
139
|
entryPoints: [entryPath],
|
|
131
140
|
bundle: true,
|
|
132
|
-
write: false,
|
|
133
141
|
platform: 'browser',
|
|
134
142
|
format: 'esm',
|
|
135
143
|
target: 'es2020',
|
|
144
|
+
write: false,
|
|
136
145
|
});
|
|
137
|
-
|
|
138
|
-
|
|
146
|
+
let code = result.outputFiles[0].text;
|
|
147
|
+
if (id === 'react-dom/client') {
|
|
148
|
+
code += `
|
|
149
|
+
import * as ReactDOMClient from '/@modules/react-dom';
|
|
150
|
+
export const createRoot = ReactDOMClient.createRoot || ReactDOMClient.default?.createRoot;
|
|
151
|
+
export default ReactDOMClient.default || ReactDOMClient;
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
await fs_extra_1.default.writeFile(cacheFile, code, 'utf8');
|
|
139
155
|
res.setHeader('Content-Type', 'application/javascript');
|
|
140
156
|
res.end(code);
|
|
141
157
|
}
|
|
142
158
|
catch (err) {
|
|
143
159
|
const msg = err instanceof Error ? err.message : String(err);
|
|
144
|
-
console.error(chalk_1.default.red(`Failed to resolve module ${id}: ${msg}`));
|
|
145
160
|
res.writeHead(500);
|
|
146
|
-
res.end(`//
|
|
161
|
+
res.end(`// Failed to resolve module ${id}: ${msg}`);
|
|
147
162
|
}
|
|
148
163
|
});
|
|
149
|
-
//
|
|
164
|
+
// --- Serve /src files dynamically
|
|
150
165
|
app.use(async (req, res, next) => {
|
|
151
|
-
if (!req.url || !req.url.startsWith('/src/'))
|
|
166
|
+
if (!req.url || (!req.url.startsWith('/src/') && !req.url.endsWith('.css')))
|
|
167
|
+
return next();
|
|
168
|
+
const filePath = path_1.default.join(appRoot, decodeURIComponent(req.url.split('?')[0]));
|
|
169
|
+
if (!(await fs_extra_1.default.pathExists(filePath)))
|
|
152
170
|
return next();
|
|
153
171
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return
|
|
172
|
+
if (transformCache.has(filePath)) {
|
|
173
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
174
|
+
return res.end(transformCache.get(filePath));
|
|
175
|
+
}
|
|
157
176
|
let code = await fs_extra_1.default.readFile(filePath, 'utf8');
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
177
|
+
for (const p of plugins) {
|
|
178
|
+
if (p.onTransform)
|
|
179
|
+
code = await p.onTransform(code, filePath);
|
|
180
|
+
}
|
|
181
|
+
const ext = path_1.default.extname(filePath);
|
|
162
182
|
let loader = 'js';
|
|
163
183
|
if (ext === '.ts')
|
|
164
184
|
loader = 'ts';
|
|
@@ -166,104 +186,96 @@ async function dev() {
|
|
|
166
186
|
loader = 'tsx';
|
|
167
187
|
else if (ext === '.jsx')
|
|
168
188
|
loader = 'jsx';
|
|
169
|
-
const
|
|
189
|
+
const result = await esbuild_1.default.transform(code, {
|
|
170
190
|
loader,
|
|
171
191
|
sourcemap: 'inline',
|
|
172
|
-
sourcefile: req.url,
|
|
173
192
|
target: 'es2020',
|
|
174
|
-
jsxFactory: 'React.createElement',
|
|
175
|
-
jsxFragment: 'React.Fragment',
|
|
176
193
|
});
|
|
194
|
+
transformCache.set(filePath, result.code);
|
|
177
195
|
res.setHeader('Content-Type', 'application/javascript');
|
|
178
|
-
res.end(
|
|
196
|
+
res.end(result.code);
|
|
179
197
|
}
|
|
180
198
|
catch (err) {
|
|
181
199
|
const msg = err instanceof Error ? err.message : String(err);
|
|
182
|
-
console.error('Error serving /src file:', msg);
|
|
183
200
|
res.writeHead(500);
|
|
184
201
|
res.end(`// Error: ${msg}`);
|
|
185
202
|
}
|
|
186
203
|
});
|
|
187
|
-
//
|
|
204
|
+
// --- Serve index.html with overlay + HMR client
|
|
188
205
|
app.use(async (req, res, next) => {
|
|
189
|
-
if (req.url
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
|
|
196
|
-
html = html.replace('</body>', `
|
|
197
|
-
<script type="module">
|
|
198
|
-
import "/@react-refresh";
|
|
199
|
-
const ws = new WebSocket("ws://" + location.host);
|
|
200
|
-
ws.onmessage = async (e) => {
|
|
201
|
-
const msg = JSON.parse(e.data);
|
|
202
|
-
if (msg.type === "error") {
|
|
203
|
-
console.error(msg);
|
|
204
|
-
return window.showErrorOverlay?.(msg);
|
|
205
|
-
}
|
|
206
|
-
if (msg.type === "update") {
|
|
207
|
-
try {
|
|
208
|
-
await import(msg.path + "?t=" + Date.now());
|
|
209
|
-
window.clearErrorOverlay?.();
|
|
210
|
-
window.$RefreshRuntime?.performReactRefresh?.();
|
|
211
|
-
} catch (err) {
|
|
212
|
-
window.showErrorOverlay?.(err);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
if (msg.type === "reload") location.reload();
|
|
216
|
-
};
|
|
217
|
-
</script>
|
|
218
|
-
</body>`);
|
|
219
|
-
res.setHeader('Content-Type', 'text/html');
|
|
220
|
-
res.end(html);
|
|
206
|
+
if (req.url !== '/' && req.url !== '/index.html')
|
|
207
|
+
return next();
|
|
208
|
+
if (!fs_extra_1.default.existsSync(indexHtml)) {
|
|
209
|
+
res.writeHead(404);
|
|
210
|
+
return res.end('index.html not found');
|
|
221
211
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
212
|
+
let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
|
|
213
|
+
html = html.replace('</body>', `
|
|
214
|
+
<script>
|
|
215
|
+
(() => {
|
|
216
|
+
const style = document.createElement('style');
|
|
217
|
+
style.textContent = \`
|
|
218
|
+
.rc-overlay {
|
|
219
|
+
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
|
220
|
+
background: rgba(0,0,0,0.9); color: #ff5555;
|
|
221
|
+
font-family: monospace; padding: 2rem; overflow:auto; z-index: 999999;
|
|
228
222
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
223
|
+
\`;
|
|
224
|
+
document.head.appendChild(style);
|
|
225
|
+
window.showErrorOverlay = (err) => {
|
|
226
|
+
window.clearErrorOverlay?.();
|
|
227
|
+
const el = document.createElement('div');
|
|
228
|
+
el.className = 'rc-overlay';
|
|
229
|
+
el.innerHTML = '<h2>šØ Error</h2><pre>' + (err.message || err) + '</pre>';
|
|
230
|
+
document.body.appendChild(el);
|
|
231
|
+
window.__overlay = el;
|
|
232
|
+
};
|
|
233
|
+
window.clearErrorOverlay = () => window.__overlay?.remove();
|
|
234
|
+
})();
|
|
235
|
+
</script>
|
|
236
|
+
<script type="module">
|
|
237
|
+
const ws = new WebSocket("ws://" + location.host);
|
|
238
|
+
ws.onmessage = (e) => {
|
|
239
|
+
const msg = JSON.parse(e.data);
|
|
240
|
+
if (msg.type === "reload") location.reload();
|
|
241
|
+
if (msg.type === "error") return window.showErrorOverlay?.(msg);
|
|
242
|
+
if (msg.type === "update") {
|
|
243
|
+
window.clearErrorOverlay?.();
|
|
244
|
+
import(msg.path + "?t=" + Date.now());
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
</script>
|
|
248
|
+
</body>`);
|
|
249
|
+
res.setHeader('Content-Type', 'text/html');
|
|
250
|
+
res.end(html);
|
|
232
251
|
});
|
|
233
|
-
//
|
|
252
|
+
// --- WebSocket + HMR via BroadcastManager
|
|
234
253
|
const server = http_1.default.createServer(app);
|
|
235
|
-
const
|
|
236
|
-
const broadcast = (data) => {
|
|
237
|
-
const json = JSON.stringify(data);
|
|
238
|
-
wss.clients.forEach((c) => c.readyState === 1 && c.send(json));
|
|
239
|
-
};
|
|
254
|
+
const broadcaster = new broadcastManager_1.BroadcastManager(server);
|
|
240
255
|
chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', async (file) => {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (err instanceof Error) {
|
|
248
|
-
broadcast({ type: 'error', message: err.message, stack: err.stack });
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
broadcast({ type: 'error', message: String(err) });
|
|
252
|
-
}
|
|
256
|
+
console.log(chalk_1.default.yellow(`š Changed: ${file}`));
|
|
257
|
+
transformCache.delete(file);
|
|
258
|
+
for (const p of plugins) {
|
|
259
|
+
p.onHotUpdate?.(file, {
|
|
260
|
+
broadcast: (msg) => broadcaster.broadcast(msg),
|
|
261
|
+
});
|
|
253
262
|
}
|
|
263
|
+
broadcaster.broadcast({
|
|
264
|
+
type: 'update',
|
|
265
|
+
path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/'),
|
|
266
|
+
});
|
|
254
267
|
});
|
|
255
|
-
//
|
|
268
|
+
// š Launch
|
|
256
269
|
server.listen(port, async () => {
|
|
257
270
|
const url = `http://localhost:${port}`;
|
|
258
|
-
console.log(chalk_1.default.cyan.bold(
|
|
259
|
-
console.log(chalk_1.default.gray('
|
|
271
|
+
console.log(chalk_1.default.cyan.bold('\nš React Client Dev Server'));
|
|
272
|
+
console.log(chalk_1.default.gray('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
260
273
|
console.log(chalk_1.default.green(`ā” Running at: ${url}`));
|
|
261
274
|
await (0, open_1.default)(url, { newInstance: true });
|
|
262
275
|
});
|
|
263
|
-
process.on('SIGINT',
|
|
276
|
+
process.on('SIGINT', () => {
|
|
264
277
|
console.log(chalk_1.default.red('\nš Shutting down...'));
|
|
265
|
-
|
|
266
|
-
server.close();
|
|
278
|
+
broadcaster.close();
|
|
267
279
|
process.exit(0);
|
|
268
280
|
});
|
|
269
281
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.BroadcastManager = void 0;
|
|
7
|
+
const ws_1 = require("ws");
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
/**
|
|
10
|
+
* BroadcastManager ā Shared WebSocket utility for dev, preview, and SSR servers.
|
|
11
|
+
* Generic over message type T which defaults to HMRMessage.
|
|
12
|
+
*/
|
|
13
|
+
class BroadcastManager {
|
|
14
|
+
constructor(server) {
|
|
15
|
+
this.clients = new Set();
|
|
16
|
+
this.wss = new ws_1.WebSocketServer({ server });
|
|
17
|
+
this.wss.on('connection', (ws) => {
|
|
18
|
+
this.clients.add(ws);
|
|
19
|
+
console.log(chalk_1.default.gray('š Client connected'));
|
|
20
|
+
ws.on('close', () => {
|
|
21
|
+
this.clients.delete(ws);
|
|
22
|
+
console.log(chalk_1.default.gray('ā Client disconnected'));
|
|
23
|
+
});
|
|
24
|
+
ws.on('error', (err) => {
|
|
25
|
+
console.error(chalk_1.default.red('ā ļø WebSocket error:'), err.message);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
broadcast(msg) {
|
|
30
|
+
const data = JSON.stringify(msg);
|
|
31
|
+
for (const client of this.clients) {
|
|
32
|
+
if (client.readyState === ws_1.WebSocket.OPEN) {
|
|
33
|
+
client.send(data);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
send(ws, msg) {
|
|
38
|
+
if (ws.readyState === ws_1.WebSocket.OPEN) {
|
|
39
|
+
ws.send(JSON.stringify(msg));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
getClientCount() {
|
|
43
|
+
return this.clients.size;
|
|
44
|
+
}
|
|
45
|
+
close() {
|
|
46
|
+
console.log(chalk_1.default.red('š Closing WebSocket connections...'));
|
|
47
|
+
this.wss.close();
|
|
48
|
+
for (const ws of this.clients) {
|
|
49
|
+
try {
|
|
50
|
+
ws.close();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// ignore
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
this.clients.clear();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.BroadcastManager = BroadcastManager;
|