react-client 1.0.23 → 1.0.24

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.
@@ -1,4 +1,16 @@
1
1
  "use strict";
2
+ /**
3
+ * dev.ts — Vite-like dev server for react-client
4
+ *
5
+ * - prebundles deps into .react-client/deps
6
+ * - serves /@modules/<dep>
7
+ * - serves /src/* with esbuild transform & inline sourcemap
8
+ * - serves /@runtime/overlay -> src/runtime/overlay-runtime.js
9
+ * - /@source-map returns a snippet for overlay mapping
10
+ * - HMR broadcast via BroadcastManager (ws)
11
+ *
12
+ * Keep this file linted & typed. Avoids manual react-dom/client hacks.
13
+ */
2
14
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
15
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
16
  };
@@ -13,54 +25,54 @@ const prompts_1 = __importDefault(require("prompts"));
13
25
  const path_1 = __importDefault(require("path"));
14
26
  const fs_extra_1 = __importDefault(require("fs-extra"));
15
27
  const open_1 = __importDefault(require("open"));
16
- const child_process_1 = require("child_process");
17
28
  const chalk_1 = __importDefault(require("chalk"));
29
+ const child_process_1 = require("child_process");
18
30
  const loadConfig_1 = require("../../utils/loadConfig");
19
31
  const broadcastManager_1 = require("../../server/broadcastManager");
32
+ const RUNTIME_OVERLAY = '/src/runtime/overlay-runtime.js';
20
33
  async function dev() {
21
34
  const root = process.cwd();
22
- const userConfig = await (0, loadConfig_1.loadReactClientConfig)(root);
35
+ const userConfig = (await (0, loadConfig_1.loadReactClientConfig)(root));
23
36
  const appRoot = path_1.default.resolve(root, userConfig.root || '.');
24
- const defaultPort = userConfig.server?.port || 5173;
37
+ const defaultPort = userConfig.server?.port ?? 2202;
38
+ // cache dir for prebundled deps
25
39
  const cacheDir = path_1.default.join(appRoot, '.react-client', 'deps');
26
- const pkgFile = path_1.default.join(appRoot, 'package.json');
27
40
  await fs_extra_1.default.ensureDir(cacheDir);
28
- // Detect entry (main.tsx or main.jsx)
29
- const possibleEntries = ['src/main.tsx', 'src/main.jsx'];
30
- const entry = possibleEntries.map((p) => path_1.default.join(appRoot, p)).find((p) => fs_extra_1.default.existsSync(p));
41
+ // Detect entry (main.tsx / main.jsx)
42
+ const possible = ['src/main.tsx', 'src/main.jsx'].map((p) => path_1.default.join(appRoot, p));
43
+ const entry = possible.find((p) => fs_extra_1.default.existsSync(p));
31
44
  if (!entry) {
32
- console.error(chalk_1.default.red('❌ No entry found: src/main.tsx or src/main.jsx'));
45
+ console.error(chalk_1.default.red('❌ Entry not found: src/main.tsx or src/main.jsx'));
33
46
  process.exit(1);
34
47
  }
35
48
  const indexHtml = path_1.default.join(appRoot, 'index.html');
36
- // Detect open port
49
+ // Select port
37
50
  const availablePort = await (0, detect_port_1.default)(defaultPort);
38
51
  const port = availablePort;
39
52
  if (availablePort !== defaultPort) {
40
- const res = await (0, prompts_1.default)({
53
+ const response = await (0, prompts_1.default)({
41
54
  type: 'confirm',
42
55
  name: 'useNewPort',
43
56
  message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
44
57
  initial: true,
45
58
  });
46
- if (!res.useNewPort) {
59
+ if (!response.useNewPort) {
47
60
  console.log('🛑 Dev server cancelled.');
48
61
  process.exit(0);
49
62
  }
50
63
  }
51
- // Ensure react-refresh installed
64
+ // Ensure react-refresh runtime available (used by many templates)
52
65
  try {
53
66
  require.resolve('react-refresh/runtime');
54
67
  }
55
68
  catch {
56
- console.warn(chalk_1.default.yellow('⚠️ react-refresh not found — installing...'));
69
+ console.warn(chalk_1.default.yellow('⚠️ react-refresh not found — installing react-refresh...'));
57
70
  (0, child_process_1.execSync)('npm install react-refresh --no-audit --no-fund --silent', {
58
- cwd: root,
71
+ cwd: appRoot,
59
72
  stdio: 'inherit',
60
73
  });
61
- console.log(chalk_1.default.green('✅ react-refresh installed successfully.'));
62
74
  }
63
- // Core + User Plugins
75
+ // Plugin system (core + user)
64
76
  const corePlugins = [
65
77
  {
66
78
  name: 'css-hmr',
@@ -69,10 +81,10 @@ async function dev() {
69
81
  const escaped = JSON.stringify(code);
70
82
  return `
71
83
  const css = ${escaped};
72
- const style = document.createElement('style');
84
+ const style = document.createElement("style");
73
85
  style.textContent = css;
74
86
  document.head.appendChild(style);
75
- import.meta.hot && import.meta.hot.accept();
87
+ import.meta.hot?.accept();
76
88
  `;
77
89
  }
78
90
  return code;
@@ -81,203 +93,316 @@ async function dev() {
81
93
  ];
82
94
  const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
83
95
  const plugins = [...corePlugins, ...userPlugins];
96
+ // App + caches
84
97
  const app = (0, connect_1.default)();
85
98
  const transformCache = new Map();
86
- // Analyze dependency graph recursively
99
+ // Helper: recursively analyze dependency graph for prebundling (bare imports)
87
100
  async function analyzeGraph(file, seen = new Set()) {
88
101
  if (seen.has(file))
89
102
  return seen;
90
103
  seen.add(file);
91
- const code = await fs_extra_1.default.readFile(file, 'utf8');
92
- const matches = [
93
- ...code.matchAll(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g),
94
- ...code.matchAll(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g),
95
- ];
96
- for (const m of matches) {
97
- const dep = m[1];
98
- if (!dep || dep.startsWith('.') || dep.startsWith('/'))
99
- continue;
100
- try {
101
- const resolved = require.resolve(dep, { paths: [appRoot] });
102
- await analyzeGraph(resolved, seen);
103
- }
104
- catch {
105
- seen.add(dep);
104
+ try {
105
+ const code = await fs_extra_1.default.readFile(file, 'utf8');
106
+ const matches = [
107
+ ...code.matchAll(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g),
108
+ ...code.matchAll(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g),
109
+ ];
110
+ for (const m of matches) {
111
+ const dep = m[1];
112
+ if (!dep || dep.startsWith('.') || dep.startsWith('/'))
113
+ continue;
114
+ try {
115
+ const resolved = require.resolve(dep, { paths: [appRoot] });
116
+ await analyzeGraph(resolved, seen);
117
+ }
118
+ catch {
119
+ // bare dependency (node_modules) - track name
120
+ seen.add(dep);
121
+ }
106
122
  }
107
123
  }
124
+ catch {
125
+ // ignore unreadable files
126
+ }
108
127
  return seen;
109
128
  }
110
- // Smart prebundling cache
129
+ // Prebundle dependencies into cache dir (parallel)
111
130
  async function prebundleDeps(deps) {
112
131
  if (!deps.size)
113
132
  return;
114
- const cached = (await fs_extra_1.default.readdir(cacheDir)).map((f) => f.replace('.js', ''));
115
- const missing = [...deps].filter((d) => !cached.includes(d));
116
- if (!missing.length) {
117
- console.log(chalk_1.default.green('✅ All dependencies already prebundled.'));
133
+ const existingFiles = await fs_extra_1.default.readdir(cacheDir);
134
+ const existing = new Set(existingFiles.map((f) => f.replace(/\.js$/, '')));
135
+ const missing = [...deps].filter((d) => !existing.has(d));
136
+ if (!missing.length)
118
137
  return;
119
- }
120
138
  console.log(chalk_1.default.cyan('📦 Prebundling:'), missing.join(', '));
121
139
  await Promise.all(missing.map(async (dep) => {
122
140
  try {
123
- const entryPath = require.resolve(dep, { paths: [appRoot] });
124
- const outFile = path_1.default.join(cacheDir, dep + '.js');
141
+ const entryPoint = require.resolve(dep, { paths: [appRoot] });
142
+ const outFile = path_1.default.join(cacheDir, dep.replace(/\//g, '_') + '.js');
125
143
  await esbuild_1.default.build({
126
- entryPoints: [entryPath],
144
+ entryPoints: [entryPoint],
127
145
  bundle: true,
128
146
  platform: 'browser',
129
147
  format: 'esm',
130
148
  outfile: outFile,
131
149
  write: true,
132
- target: 'es2020',
150
+ target: ['es2020'],
133
151
  });
134
152
  console.log(chalk_1.default.green(`✅ Cached ${dep}`));
135
153
  }
136
154
  catch (err) {
137
- const e = err;
138
- console.warn(chalk_1.default.yellow(`⚠️ Skipped ${dep}: ${e.message}`));
155
+ console.warn(chalk_1.default.yellow(`⚠️ Skipped ${dep}: ${err.message}`));
139
156
  }
140
157
  }));
141
158
  }
142
- // Initial dependency prebundle
143
- const deps = await analyzeGraph(entry);
144
- await prebundleDeps(deps);
145
- // Auto re-prebundle when package.json changes
146
- chokidar_1.default.watch(pkgFile).on('change', async () => {
147
- console.log(chalk_1.default.yellow('📦 package.json changed — rebuilding prebundle cache...'));
148
- const newDeps = await analyzeGraph(entry);
149
- await prebundleDeps(newDeps);
150
- });
151
- // ✅ Serve /@modules/
152
- app.use('/@modules/', async (req, res, next) => {
153
- const id = req.url?.replace(/^\/(@modules\/)?/, '');
154
- if (!id)
159
+ // Build initial prebundle graph from entry
160
+ const depsSet = await analyzeGraph(entry);
161
+ await prebundleDeps(depsSet);
162
+ // Watch package.json for changes to re-prebundle
163
+ const pkgPath = path_1.default.join(appRoot, 'package.json');
164
+ if (await fs_extra_1.default.pathExists(pkgPath)) {
165
+ chokidar_1.default.watch(pkgPath).on('change', async () => {
166
+ console.log(chalk_1.default.yellow('📦 package.json changed — rebuilding prebundle...'));
167
+ const newDeps = await analyzeGraph(entry);
168
+ await prebundleDeps(newDeps);
169
+ });
170
+ }
171
+ // --- Serve /@modules/<dep> (prebundled or on-demand esbuild bundle)
172
+ app.use((async (req, res, next) => {
173
+ const url = req.url ?? '';
174
+ if (!url.startsWith('/@modules/'))
155
175
  return next();
176
+ const id = url.replace(/^\/@modules\//, '');
177
+ if (!id) {
178
+ res.writeHead(400);
179
+ return res.end('// invalid module');
180
+ }
156
181
  try {
157
- const cacheFile = path_1.default.join(cacheDir, id.replace(/\//g, '_') + '.js');
182
+ const cacheFile = path_1.default.join(cacheDir, id.replace(/[\\/]/g, '_') + '.js');
158
183
  if (await fs_extra_1.default.pathExists(cacheFile)) {
159
184
  res.setHeader('Content-Type', 'application/javascript');
160
- return res.end(await fs_extra_1.default.readFile(cacheFile));
185
+ res.end(await fs_extra_1.default.readFile(cacheFile, 'utf8'));
186
+ return;
161
187
  }
162
- const entryPath = require.resolve(id, { paths: [appRoot] });
188
+ // Resolve and bundle on-demand
189
+ const entryResolved = require.resolve(id, { paths: [appRoot] });
163
190
  const result = await esbuild_1.default.build({
164
- entryPoints: [entryPath],
191
+ entryPoints: [entryResolved],
165
192
  bundle: true,
193
+ write: false,
166
194
  platform: 'browser',
167
195
  format: 'esm',
168
- target: 'es2020',
169
- write: false,
196
+ target: ['es2020'],
170
197
  });
171
- const code = result.outputFiles[0].text;
172
- await fs_extra_1.default.writeFile(cacheFile, code, 'utf8');
198
+ const output = result.outputFiles?.[0]?.text ?? '';
199
+ // Persist to cache so next request is faster
200
+ await fs_extra_1.default.writeFile(cacheFile, output, 'utf8');
173
201
  res.setHeader('Content-Type', 'application/javascript');
174
- res.end(code);
202
+ res.end(output);
175
203
  }
176
204
  catch (err) {
177
- const e = err;
178
- console.error(chalk_1.default.red(`❌ Failed to load module ${id}: ${e.message}`));
179
205
  res.writeHead(500);
180
- res.end(`// Failed to resolve module ${id}: ${e.message}`);
206
+ res.end(`// Failed to resolve module ${id}: ${err.message}`);
181
207
  }
182
- });
183
- // Serve /src files dynamically
184
- app.use(async (req, res, next) => {
185
- if (!req.url || (!req.url.startsWith('/src/') && !req.url.endsWith('.css')))
208
+ }));
209
+ // --- Serve runtime overlay (local file) so overlay-runtime.js is loaded automatically
210
+ app.use((async (req, res, next) => {
211
+ const url = req.url ?? '';
212
+ if (url !== '/@runtime/overlay')
213
+ return next();
214
+ const overlayPath = path_1.default.join(appRoot, RUNTIME_OVERLAY);
215
+ if (!(await fs_extra_1.default.pathExists(overlayPath))) {
216
+ res.writeHead(404);
217
+ return res.end('// overlay-runtime not found');
218
+ }
219
+ res.setHeader('Content-Type', 'application/javascript');
220
+ res.end(await fs_extra_1.default.readFile(overlayPath, 'utf8'));
221
+ }));
222
+ // --- minimal /@source-map: return snippet around requested line of original source file
223
+ app.use((async (req, res, next) => {
224
+ const url = req.url ?? '';
225
+ if (!url.startsWith('/@source-map'))
186
226
  return next();
187
- const rawPath = decodeURIComponent(req.url.split('?')[0]);
188
- let filePath = path_1.default.join(appRoot, rawPath);
189
- const possibleExts = ['', '.tsx', '.ts', '.jsx', '.js'];
190
- for (const ext of possibleExts) {
227
+ // expected query: ?file=/src/xyz.tsx&line=12&column=3
228
+ try {
229
+ const full = req.url ?? '';
230
+ const parsed = new URL(full, `http://localhost:${port}`);
231
+ const file = parsed.searchParams.get('file') ?? '';
232
+ const lineStr = parsed.searchParams.get('line') ?? '0';
233
+ const lineNum = Number(lineStr) || 0;
234
+ if (!file) {
235
+ res.writeHead(400);
236
+ return res.end('{}');
237
+ }
238
+ const filePath = path_1.default.join(appRoot, file.startsWith('/') ? file.slice(1) : file);
239
+ if (!(await fs_extra_1.default.pathExists(filePath))) {
240
+ res.writeHead(404);
241
+ return res.end('{}');
242
+ }
243
+ const src = await fs_extra_1.default.readFile(filePath, 'utf8');
244
+ const lines = src.split(/\r?\n/);
245
+ const start = Math.max(0, lineNum - 3 - 1);
246
+ const end = Math.min(lines.length, lineNum + 2);
247
+ const snippet = lines
248
+ .slice(start, end)
249
+ .map((l, i) => {
250
+ const ln = start + i + 1;
251
+ return `<span class="line-number">${ln}</span> ${l
252
+ .replace(/</g, '&lt;')
253
+ .replace(/>/g, '&gt;')}`;
254
+ })
255
+ .join('\n');
256
+ res.setHeader('Content-Type', 'application/json');
257
+ res.end(JSON.stringify({ source: file, line: lineNum, column: 0, snippet }));
258
+ }
259
+ catch (err) {
260
+ res.writeHead(500);
261
+ res.end(JSON.stringify({ error: err.message }));
262
+ }
263
+ }));
264
+ // --- Serve /src/* files (on-the-fly transform + bare import rewrite)
265
+ app.use((async (req, res, next) => {
266
+ const url = req.url ?? '';
267
+ if (!url.startsWith('/src/') && !url.endsWith('.css'))
268
+ return next();
269
+ const raw = decodeURIComponent((req.url ?? '').split('?')[0]);
270
+ const filePath = path_1.default.join(appRoot, raw.replace(/^\//, ''));
271
+ // Try file extensions if not exact file
272
+ const exts = ['', '.tsx', '.ts', '.jsx', '.js', '.css'];
273
+ let found = '';
274
+ for (const ext of exts) {
191
275
  if (await fs_extra_1.default.pathExists(filePath + ext)) {
192
- filePath += ext;
276
+ found = filePath + ext;
193
277
  break;
194
278
  }
195
279
  }
196
- if (!(await fs_extra_1.default.pathExists(filePath)))
280
+ if (!found)
197
281
  return next();
198
282
  try {
199
- let code = await fs_extra_1.default.readFile(filePath, 'utf8');
200
- // Rewrite bare imports /@modules/*
283
+ let code = await fs_extra_1.default.readFile(found, 'utf8');
284
+ // rewrite bare imports -> /@modules/<dep>
201
285
  code = code
202
286
  .replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
203
287
  .replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
204
- for (const p of plugins)
205
- if (p.onTransform)
206
- code = await p.onTransform(code, filePath);
207
- const ext = path_1.default.extname(filePath);
208
- let loader = 'js';
209
- if (ext === '.ts')
210
- loader = 'ts';
211
- else if (ext === '.tsx')
212
- loader = 'tsx';
213
- else if (ext === '.jsx')
214
- loader = 'jsx';
288
+ // run plugin transforms
289
+ for (const p of plugins) {
290
+ if (p.onTransform) {
291
+ // plugin may return transformed code
292
+ // keep typed as string
293
+ // eslint-disable-next-line no-await-in-loop
294
+ const out = await p.onTransform(code, found);
295
+ if (typeof out === 'string')
296
+ code = out;
297
+ }
298
+ }
299
+ // choose loader by extension
300
+ const ext = path_1.default.extname(found).toLowerCase();
301
+ const loader = ext === '.ts' ? 'ts' : ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'js';
215
302
  const result = await esbuild_1.default.transform(code, {
216
303
  loader,
217
304
  sourcemap: 'inline',
218
- target: 'es2020',
305
+ target: ['es2020'],
219
306
  });
220
- transformCache.set(filePath, result.code);
307
+ transformCache.set(found, result.code);
221
308
  res.setHeader('Content-Type', 'application/javascript');
222
309
  res.end(result.code);
223
310
  }
224
311
  catch (err) {
225
312
  const e = err;
226
- console.error(chalk_1.default.red(`⚠️ Transform failed: ${e.message}`));
227
313
  res.writeHead(500);
228
- res.end(`// Error: ${e.message}`);
314
+ res.end(`// transform error: ${e.message}`);
229
315
  }
230
- });
231
- // Serve index.html + overlay + runtime
232
- app.use(async (req, res, next) => {
233
- if (req.url !== '/' && req.url !== '/index.html')
316
+ }));
317
+ // --- Serve index.html with overlay + HMR client injection
318
+ app.use((async (req, res, next) => {
319
+ const url = req.url ?? '';
320
+ if (url !== '/' && url !== '/index.html')
234
321
  return next();
235
- if (!fs_extra_1.default.existsSync(indexHtml)) {
322
+ if (!(await fs_extra_1.default.pathExists(indexHtml))) {
236
323
  res.writeHead(404);
237
324
  return res.end('index.html not found');
238
325
  }
239
- let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
240
- html = html.replace('</body>', `
241
- <script type="module" src="/src/runtime/overlay-runtime.js"></script>
242
- <script type="module">
243
- const ws = new WebSocket("ws://" + location.host);
244
- ws.onmessage = (e) => {
245
- const msg = JSON.parse(e.data);
246
- if (msg.type === "reload") location.reload();
247
- if (msg.type === "error") return window.showErrorOverlay?.(msg);
248
- if (msg.type === "update") {
249
- window.clearErrorOverlay?.();
250
- import(msg.path + "?t=" + Date.now());
251
- }
252
- };
253
- </script>
254
- </body>`);
255
- res.setHeader('Content-Type', 'text/html');
256
- res.end(html);
257
- });
258
- // ✅ WebSocket + HMR
326
+ try {
327
+ let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
328
+ // inject overlay runtime and HMR client if not already present
329
+ if (!html.includes('/@runtime/overlay')) {
330
+ html = html.replace('</body>', `\n<script type="module" src="/@runtime/overlay"></script>\n<script type="module">
331
+ const ws = new WebSocket("ws://" + location.host);
332
+ ws.onmessage = (e) => {
333
+ const msg = JSON.parse(e.data);
334
+ if (msg.type === "reload") location.reload();
335
+ if (msg.type === "error") window.showErrorOverlay?.(msg);
336
+ if (msg.type === "update") {
337
+ window.clearErrorOverlay?.();
338
+ import(msg.path + "?t=" + Date.now());
339
+ }
340
+ };
341
+ </script>\n</body>`);
342
+ }
343
+ res.setHeader('Content-Type', 'text/html');
344
+ res.end(html);
345
+ }
346
+ catch (err) {
347
+ res.writeHead(500);
348
+ res.end(`// html read error: ${err.message}`);
349
+ }
350
+ }));
351
+ // --- HMR WebSocket server
259
352
  const server = http_1.default.createServer(app);
260
353
  const broadcaster = new broadcastManager_1.BroadcastManager(server);
261
- chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', async (file) => {
262
- console.log(chalk_1.default.yellow(`🔄 Changed: ${file}`));
354
+ // Watch files and trigger plugin onHotUpdate + broadcast HMR message
355
+ const watcher = chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true });
356
+ watcher.on('change', async (file) => {
263
357
  transformCache.delete(file);
264
- for (const p of plugins)
265
- await p.onHotUpdate?.(file, { broadcast: (msg) => broadcaster.broadcast(msg) });
358
+ // plugin hook onHotUpdate optionally
359
+ for (const p of plugins) {
360
+ if (p.onHotUpdate) {
361
+ try {
362
+ // allow plugin to broadcast via a simple function
363
+ // plugin gets { broadcast }
364
+ // plugin signature: onHotUpdate(file, { broadcast })
365
+ // eslint-disable-next-line no-await-in-loop
366
+ await p.onHotUpdate(file, {
367
+ broadcast: (msg) => {
368
+ broadcaster.broadcast(msg);
369
+ },
370
+ });
371
+ }
372
+ catch (err) {
373
+ // plugin errors shouldn't crash server
374
+ // eslint-disable-next-line no-console
375
+ console.warn('plugin onHotUpdate error:', err.message);
376
+ }
377
+ }
378
+ }
379
+ // default: broadcast update for changed file
266
380
  broadcaster.broadcast({
267
381
  type: 'update',
268
382
  path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/'),
269
383
  });
270
384
  });
385
+ // start server
271
386
  server.listen(port, async () => {
272
387
  const url = `http://localhost:${port}`;
273
388
  console.log(chalk_1.default.cyan.bold('\n🚀 React Client Dev Server'));
274
- console.log(chalk_1.default.gray('──────────────────────────────'));
275
389
  console.log(chalk_1.default.green(`⚡ Running at: ${url}`));
276
- await (0, open_1.default)(url, { newInstance: true });
390
+ if (userConfig.server?.open !== false) {
391
+ // open default browser
392
+ try {
393
+ await (0, open_1.default)(url);
394
+ }
395
+ catch {
396
+ // ignore open errors
397
+ }
398
+ }
277
399
  });
278
- process.on('SIGINT', () => {
400
+ // graceful shutdown
401
+ process.on('SIGINT', async () => {
279
402
  console.log(chalk_1.default.red('\n🛑 Shutting down...'));
403
+ watcher.close();
280
404
  broadcaster.close();
405
+ server.close();
281
406
  process.exit(0);
282
407
  });
283
408
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-client",
3
- "version": "1.0.23",
3
+ "version": "1.0.24",
4
4
  "description": "react-client is a lightweight CLI and runtime for building React apps with fast iteration.",
5
5
  "license": "MIT",
6
6
  "author": "Venkatesh Sundaram",