react-client 1.0.23 → 1.0.25

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