react-client 1.0.32 → 1.0.33

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,13 +1,14 @@
1
+ // src/cli/commands/dev.ts
1
2
  /**
2
- * dev.ts — dev server for react-client
3
+ * dev.ts — Vite-like dev server for react-client
3
4
  *
4
- * - prebundles deps into .react-client/deps
5
- * - serves /@modules/<dep>
6
- * - serves /src/* with esbuild transform & inline sourcemap
7
- * - /@source-map returns a snippet for overlay mapping
8
- * - HMR broadcast via BroadcastManager (ws)
9
- *
10
- * Keep this file linted & typed. Avoids manual react-dom/client hacks.
5
+ * Features:
6
+ * - prebundles deps into .react-client/deps (persistent)
7
+ * - serves /@modules/<dep> (prebundled or on-demand esbuild bundle)
8
+ * - serves /src/* with esbuild transform + inline sourcemap
9
+ * - /@source-map returns a snippet for overlay mapping
10
+ * - HMR broadcast via BroadcastManager (ws)
11
+ * - plugin system: onTransform, onHotUpdate, onServe, onServerStart
11
12
  */
12
13
  import esbuild from 'esbuild';
13
14
  import connect from 'connect';
@@ -20,15 +21,8 @@ import fs from 'fs-extra';
20
21
  import open from 'open';
21
22
  import chalk from 'chalk';
22
23
  import { execSync } from 'child_process';
24
+ import { loadReactClientConfig } from '../../utils/loadConfig';
23
25
  import { BroadcastManager } from '../../server/broadcastManager';
24
- import { createRequire } from 'module';
25
- import { fileURLToPath } from 'url';
26
- import { dirname, resolve } from 'path';
27
- const __filename = fileURLToPath(import.meta.url);
28
- const __dirname = dirname(__filename);
29
- const loadConfigPath = resolve(__dirname, '../../utils/loadConfig.js');
30
- const { loadReactClientConfig } = await import(loadConfigPath);
31
- const require = createRequire(import.meta.url);
32
26
  const RUNTIME_OVERLAY_ROUTE = '/@runtime/overlay';
33
27
  function jsContentType() {
34
28
  return 'application/javascript; charset=utf-8';
@@ -179,33 +173,32 @@ export default async function dev() {
179
173
  const userConfig = (await loadReactClientConfig(root));
180
174
  const appRoot = path.resolve(root, userConfig.root || '.');
181
175
  const defaultPort = userConfig.server?.port ?? 2202;
182
- // cache dir for prebundled deps
183
176
  const cacheDir = path.join(appRoot, '.react-client', 'deps');
184
177
  await fs.ensureDir(cacheDir);
185
178
  // Detect entry (main.tsx / main.jsx)
186
- const possible = ['src/main.tsx', 'src/main.jsx'].map((p) => path.join(appRoot, p));
187
- const entry = possible.find((p) => fs.existsSync(p));
179
+ const possibleEntries = ['src/main.tsx', 'src/main.jsx'].map((p) => path.join(appRoot, p));
180
+ const entry = possibleEntries.find((p) => fs.existsSync(p));
188
181
  if (!entry) {
189
182
  console.error(chalk.red('❌ Entry not found: src/main.tsx or src/main.jsx'));
190
183
  process.exit(1);
191
184
  }
192
185
  const indexHtml = path.join(appRoot, 'index.html');
193
- // Select port
186
+ // Port
194
187
  const availablePort = await detectPort(defaultPort);
195
188
  const port = availablePort;
196
189
  if (availablePort !== defaultPort) {
197
- const response = await prompts({
190
+ const res = await prompts({
198
191
  type: 'confirm',
199
192
  name: 'useNewPort',
200
193
  message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
201
194
  initial: true,
202
195
  });
203
- if (!response.useNewPort) {
196
+ if (!res.useNewPort) {
204
197
  console.log('🛑 Dev server cancelled.');
205
198
  process.exit(0);
206
199
  }
207
200
  }
208
- // Ensure react-refresh runtime available (used by many templates)
201
+ // ensure react-refresh runtime exists (templates often import it)
209
202
  try {
210
203
  require.resolve('react-refresh/runtime');
211
204
  }
@@ -218,10 +211,10 @@ export default async function dev() {
218
211
  });
219
212
  }
220
213
  catch {
221
- console.warn(chalk.yellow('⚠️ automatic install of react-refresh failed; continuing without it.'));
214
+ console.warn(chalk.yellow('⚠️ auto-install failed — please install react-refresh manually.'));
222
215
  }
223
216
  }
224
- // Plugin system (core + user)
217
+ // Core plugins
225
218
  const corePlugins = [
226
219
  {
227
220
  name: 'css-hmr',
@@ -230,10 +223,10 @@ export default async function dev() {
230
223
  const escaped = JSON.stringify(code);
231
224
  return `
232
225
  const css = ${escaped};
233
- const style = document.createElement("style");
226
+ const style = document.createElement('style');
234
227
  style.textContent = css;
235
228
  document.head.appendChild(style);
236
- import.meta.hot?.accept();
229
+ if (import.meta.hot) import.meta.hot.accept();
237
230
  `;
238
231
  }
239
232
  return code;
@@ -265,30 +258,31 @@ export default async function dev() {
265
258
  await analyzeGraph(resolved, seen);
266
259
  }
267
260
  catch {
268
- // bare dependency (node_modules) - track name
261
+ // bare dependency
269
262
  seen.add(dep);
270
263
  }
271
264
  }
272
265
  }
273
266
  catch {
274
- // ignore unreadable files
267
+ // ignore unreadable
275
268
  }
276
269
  return seen;
277
270
  }
278
- // Prebundle dependencies into cache dir (parallel)
271
+ // --- Prebundle missing deps (parallel)
279
272
  async function prebundleDeps(deps) {
280
273
  if (!deps.size)
281
274
  return;
282
- const existingFiles = await fs.readdir(cacheDir);
283
- const existing = new Set(existingFiles.map((f) => f.replace(/\.js$/, '')));
284
- const missing = [...deps].filter((d) => !existing.has(d));
285
- if (!missing.length)
275
+ const cached = new Set((await fs.readdir(cacheDir)).map((f) => f.replace(/\.js$/, '')));
276
+ const missing = [...deps].filter((d) => !cached.has(d.replace(/\//g, '_')));
277
+ if (!missing.length) {
278
+ console.log(chalk.green('✅ All dependencies prebundled.'));
286
279
  return;
280
+ }
287
281
  console.log(chalk.cyan('📦 Prebundling:'), missing.join(', '));
288
282
  await Promise.all(missing.map(async (dep) => {
289
283
  try {
290
284
  const entryPoint = require.resolve(dep, { paths: [appRoot] });
291
- const outFile = path.join(cacheDir, normalizeCacheKey(dep) + '.js');
285
+ const outFile = path.join(cacheDir, dep.replace(/\//g, '_') + '.js');
292
286
  await esbuild.build({
293
287
  entryPoints: [entryPoint],
294
288
  bundle: true,
@@ -305,10 +299,10 @@ export default async function dev() {
305
299
  }
306
300
  }));
307
301
  }
308
- // Build initial prebundle graph from entry
302
+ // initial prebundle
309
303
  const depsSet = await analyzeGraph(entry);
310
304
  await prebundleDeps(depsSet);
311
- // Watch package.json for changes to re-prebundle
305
+ // re-prebundle on package.json changes
312
306
  const pkgPath = path.join(appRoot, 'package.json');
313
307
  if (await fs.pathExists(pkgPath)) {
314
308
  chokidar.watch(pkgPath).on('change', async () => {
@@ -435,10 +429,10 @@ const overlayId = "__rc_error_overlay__";
435
429
  if (!url.startsWith('/@source-map'))
436
430
  return next();
437
431
  try {
438
- const parsed = new URL(req.url ?? '', `http://localhost:${port}`);
432
+ const full = req.url ?? '';
433
+ const parsed = new URL(full, `http://localhost:${port}`);
439
434
  const file = parsed.searchParams.get('file') ?? '';
440
- const lineStr = parsed.searchParams.get('line') ?? '0';
441
- const lineNum = Number(lineStr) || 0;
435
+ const lineNum = Number(parsed.searchParams.get('line') ?? '0') || 0;
442
436
  if (!file) {
443
437
  res.writeHead(400);
444
438
  return res.end('{}');
@@ -469,39 +463,45 @@ const overlayId = "__rc_error_overlay__";
469
463
  res.end(JSON.stringify({ error: err.message }));
470
464
  }
471
465
  }));
472
- // --- Serve /src/* files (on-the-fly transform + bare import rewrite)
466
+ // Serve /src/* files (transform on the fly)
473
467
  app.use((async (req, res, next) => {
474
468
  const url = req.url ?? '';
475
469
  if (!url.startsWith('/src/') && !url.endsWith('.css'))
476
470
  return next();
477
471
  const raw = decodeURIComponent((req.url ?? '').split('?')[0]);
478
- const filePath = path.join(appRoot, raw.replace(/^\//, ''));
479
- // Try file extensions if not exact file
472
+ const filePathBase = path.join(appRoot, raw.replace(/^\//, ''));
480
473
  const exts = ['', '.tsx', '.ts', '.jsx', '.js', '.css'];
481
474
  let found = '';
482
475
  for (const ext of exts) {
483
- if (await fs.pathExists(filePath + ext)) {
484
- found = filePath + ext;
476
+ if (await fs.pathExists(filePathBase + ext)) {
477
+ found = filePathBase + ext;
485
478
  break;
486
479
  }
487
480
  }
488
481
  if (!found)
489
482
  return next();
490
483
  try {
484
+ // serve cached transform if present
485
+ if (transformCache.has(found)) {
486
+ res.setHeader('Content-Type', 'application/javascript');
487
+ res.end(transformCache.get(found));
488
+ return;
489
+ }
491
490
  let code = await fs.readFile(found, 'utf8');
492
- // rewrite bare imports -> /@modules/<dep>
491
+ // rewrite bare imports to /@modules/*
493
492
  code = code
494
493
  .replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
495
494
  .replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
496
- // run plugin transforms
495
+ // plugin transforms
497
496
  for (const p of plugins) {
498
497
  if (p.onTransform) {
498
+ // eslint-disable-next-line no-await-in-loop
499
499
  const out = await p.onTransform(code, found);
500
500
  if (typeof out === 'string')
501
501
  code = out;
502
502
  }
503
503
  }
504
- // choose loader by extension
504
+ // choose loader by ext
505
505
  const ext = path.extname(found).toLowerCase();
506
506
  const loader = ext === '.ts' ? 'ts' : ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'js';
507
507
  const result = await esbuild.transform(code, {
@@ -519,7 +519,7 @@ const overlayId = "__rc_error_overlay__";
519
519
  res.end(`// transform error: ${e.message}`);
520
520
  }
521
521
  }));
522
- // --- Serve index.html with overlay + HMR client injection
522
+ // Serve index.html with HMR + overlay injection
523
523
  app.use((async (req, res, next) => {
524
524
  const url = req.url ?? '';
525
525
  if (url !== '/' && url !== '/index.html')
@@ -530,7 +530,7 @@ const overlayId = "__rc_error_overlay__";
530
530
  }
531
531
  try {
532
532
  let html = await fs.readFile(indexHtml, 'utf8');
533
- // inject overlay runtime and HMR client if not already present
533
+ // inject runtime overlay and HMR client if not present
534
534
  if (!html.includes(RUNTIME_OVERLAY_ROUTE)) {
535
535
  html = html.replace('</body>', `\n<script type="module" src="${RUNTIME_OVERLAY_ROUTE}"></script>\n<script type="module">
536
536
  const ws = new WebSocket("ws://" + location.host);
@@ -553,30 +553,29 @@ const overlayId = "__rc_error_overlay__";
553
553
  res.end(`// html read error: ${err.message}`);
554
554
  }
555
555
  }));
556
- // --- HMR WebSocket server
556
+ // HMR WebSocket
557
557
  const server = http.createServer(app);
558
558
  const broadcaster = new BroadcastManager(server);
559
- // Watch files and trigger plugin onHotUpdate + broadcast HMR message
559
+ // Watcher: src
560
560
  const watcher = chokidar.watch(path.join(appRoot, 'src'), { ignoreInitial: true });
561
561
  watcher.on('change', async (file) => {
562
562
  transformCache.delete(file);
563
- // plugin hook onHotUpdate optionally
563
+ // plugin hook onHotUpdate
564
564
  for (const p of plugins) {
565
565
  if (p.onHotUpdate) {
566
566
  try {
567
+ // eslint-disable-next-line no-await-in-loop
567
568
  await p.onHotUpdate(file, {
568
- // plugin only needs broadcast in most cases
569
- broadcast: (msg) => {
570
- broadcaster.broadcast(msg);
571
- },
569
+ broadcast: (m) => broadcaster.broadcast(m),
572
570
  });
573
571
  }
574
572
  catch (err) {
573
+ // plugin errors shouldn't crash server
574
+ // eslint-disable-next-line no-console
575
575
  console.warn('plugin onHotUpdate error:', err.message);
576
576
  }
577
577
  }
578
578
  }
579
- // default: broadcast update for changed file
580
579
  broadcaster.broadcast({
581
580
  type: 'update',
582
581
  path: '/' + path.relative(appRoot, file).replace(/\\/g, '/'),
@@ -586,20 +585,39 @@ const overlayId = "__rc_error_overlay__";
586
585
  server.listen(port, async () => {
587
586
  const url = `http://localhost:${port}`;
588
587
  console.log(chalk.cyan.bold('\n🚀 React Client Dev Server'));
588
+ console.log(chalk.gray('──────────────────────────────'));
589
589
  console.log(chalk.green(`⚡ Running at: ${url}`));
590
+ // open if not explicitly disabled
590
591
  if (userConfig.server?.open !== false) {
591
592
  try {
592
593
  await open(url);
593
594
  }
594
595
  catch {
595
- // ignore open errors
596
+ /* ignore */
597
+ }
598
+ }
599
+ const ctx = {
600
+ root: appRoot,
601
+ outDir: cacheDir,
602
+ app,
603
+ wss: broadcaster.wss,
604
+ httpServer: server,
605
+ broadcast: (m) => broadcaster.broadcast(m),
606
+ };
607
+ // plugin onServe / onServerStart hooks
608
+ for (const p of plugins) {
609
+ if (p.onServe) {
610
+ await p.onServe(ctx);
611
+ }
612
+ if (p.onServerStart) {
613
+ await p.onServerStart(ctx);
596
614
  }
597
615
  }
598
616
  });
599
617
  // graceful shutdown
600
618
  process.on('SIGINT', async () => {
601
619
  console.log(chalk.red('\n🛑 Shutting down...'));
602
- await watcher.close();
620
+ watcher.close();
603
621
  broadcaster.close();
604
622
  server.close();
605
623
  process.exit(0);
@@ -1,8 +1,9 @@
1
- import { WebSocketServer, WebSocket as NodeWebSocket } from 'ws';
1
+ // src/server/broadcastManager.ts
2
+ import { WebSocketServer, WebSocket } from 'ws';
2
3
  import chalk from 'chalk';
3
4
  /**
4
- * BroadcastManager — Shared WebSocket utility for dev, preview, and SSR servers.
5
- * Generic over message type T which defaults to HMRMessage.
5
+ * BroadcastManager — shared WebSocket utility for dev/preview/ssr servers.
6
+ * Uses `ws` WebSocket instances (Node) not DOM WebSocket.
6
7
  */
7
8
  export class BroadcastManager {
8
9
  constructor(server) {
@@ -10,39 +11,58 @@ export class BroadcastManager {
10
11
  this.wss = new WebSocketServer({ server });
11
12
  this.wss.on('connection', (ws) => {
12
13
  this.clients.add(ws);
14
+ console.log(chalk.gray('🔌 HMR client connected'));
13
15
  ws.on('close', () => {
14
16
  this.clients.delete(ws);
17
+ console.log(chalk.gray('❎ HMR client disconnected'));
15
18
  });
16
19
  ws.on('error', (err) => {
17
- console.error(chalk.red('⚠️ WebSocket error:'), err.message);
20
+ console.error(chalk.red('⚠️ WebSocket error:'), err?.message ?? err);
18
21
  });
19
22
  });
20
23
  }
24
+ /**
25
+ * Broadcast a message to all connected clients.
26
+ */
21
27
  broadcast(msg) {
22
28
  const data = JSON.stringify(msg);
23
29
  for (const client of this.clients) {
24
- if (client.readyState === NodeWebSocket.OPEN) {
25
- client.send(data);
30
+ // ws.OPEN === 1
31
+ if (client.readyState === WebSocket.OPEN) {
32
+ try {
33
+ client.send(data);
34
+ }
35
+ catch {
36
+ // ignore send errors per-client
37
+ }
26
38
  }
27
39
  }
28
40
  }
41
+ /**
42
+ * Send a message to a single client (ws instance from 'ws').
43
+ */
29
44
  send(ws, msg) {
30
- if (ws.readyState === NodeWebSocket.OPEN) {
45
+ if (ws.readyState === WebSocket.OPEN) {
31
46
  ws.send(JSON.stringify(msg));
32
47
  }
33
48
  }
34
- getClientCount() {
35
- return this.clients.size;
36
- }
49
+ /**
50
+ * Close all WebSocket connections and server.
51
+ */
37
52
  close() {
38
- console.log(chalk.red('🛑 Closing WebSocket connections...'));
39
- this.wss.close();
53
+ try {
54
+ console.log(chalk.red('🛑 Closing WebSocket connections...'));
55
+ this.wss.close();
56
+ }
57
+ catch {
58
+ /* ignore */
59
+ }
40
60
  for (const ws of this.clients) {
41
61
  try {
42
62
  ws.close();
43
63
  }
44
64
  catch {
45
- // ignore
65
+ // ignore per-client close errors
46
66
  }
47
67
  }
48
68
  this.clients.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-client",
3
- "version": "1.0.32",
3
+ "version": "1.0.33",
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",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "sideEffects": false,
40
40
  "files": [
41
- "dist",
41
+ "dist/**/*",
42
42
  "templates"
43
43
  ],
44
44
  "workspaces": [