react-client 1.0.31 → 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.
- package/dist/cli/commands/dev.js +77 -54
- package/dist/server/broadcastManager.js +33 -13
- package/package.json +2 -2
package/dist/cli/commands/dev.js
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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';
|
|
@@ -22,8 +23,6 @@ import chalk from 'chalk';
|
|
|
22
23
|
import { execSync } from 'child_process';
|
|
23
24
|
import { loadReactClientConfig } from '../../utils/loadConfig';
|
|
24
25
|
import { BroadcastManager } from '../../server/broadcastManager';
|
|
25
|
-
import { createRequire } from 'module';
|
|
26
|
-
const require = createRequire(import.meta.url);
|
|
27
26
|
const RUNTIME_OVERLAY_ROUTE = '/@runtime/overlay';
|
|
28
27
|
function jsContentType() {
|
|
29
28
|
return 'application/javascript; charset=utf-8';
|
|
@@ -174,33 +173,32 @@ export default async function dev() {
|
|
|
174
173
|
const userConfig = (await loadReactClientConfig(root));
|
|
175
174
|
const appRoot = path.resolve(root, userConfig.root || '.');
|
|
176
175
|
const defaultPort = userConfig.server?.port ?? 2202;
|
|
177
|
-
// cache dir for prebundled deps
|
|
178
176
|
const cacheDir = path.join(appRoot, '.react-client', 'deps');
|
|
179
177
|
await fs.ensureDir(cacheDir);
|
|
180
178
|
// Detect entry (main.tsx / main.jsx)
|
|
181
|
-
const
|
|
182
|
-
const entry =
|
|
179
|
+
const possibleEntries = ['src/main.tsx', 'src/main.jsx'].map((p) => path.join(appRoot, p));
|
|
180
|
+
const entry = possibleEntries.find((p) => fs.existsSync(p));
|
|
183
181
|
if (!entry) {
|
|
184
182
|
console.error(chalk.red('❌ Entry not found: src/main.tsx or src/main.jsx'));
|
|
185
183
|
process.exit(1);
|
|
186
184
|
}
|
|
187
185
|
const indexHtml = path.join(appRoot, 'index.html');
|
|
188
|
-
//
|
|
186
|
+
// Port
|
|
189
187
|
const availablePort = await detectPort(defaultPort);
|
|
190
188
|
const port = availablePort;
|
|
191
189
|
if (availablePort !== defaultPort) {
|
|
192
|
-
const
|
|
190
|
+
const res = await prompts({
|
|
193
191
|
type: 'confirm',
|
|
194
192
|
name: 'useNewPort',
|
|
195
193
|
message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
|
|
196
194
|
initial: true,
|
|
197
195
|
});
|
|
198
|
-
if (!
|
|
196
|
+
if (!res.useNewPort) {
|
|
199
197
|
console.log('🛑 Dev server cancelled.');
|
|
200
198
|
process.exit(0);
|
|
201
199
|
}
|
|
202
200
|
}
|
|
203
|
-
//
|
|
201
|
+
// ensure react-refresh runtime exists (templates often import it)
|
|
204
202
|
try {
|
|
205
203
|
require.resolve('react-refresh/runtime');
|
|
206
204
|
}
|
|
@@ -213,10 +211,10 @@ export default async function dev() {
|
|
|
213
211
|
});
|
|
214
212
|
}
|
|
215
213
|
catch {
|
|
216
|
-
console.warn(chalk.yellow('⚠️
|
|
214
|
+
console.warn(chalk.yellow('⚠️ auto-install failed — please install react-refresh manually.'));
|
|
217
215
|
}
|
|
218
216
|
}
|
|
219
|
-
//
|
|
217
|
+
// Core plugins
|
|
220
218
|
const corePlugins = [
|
|
221
219
|
{
|
|
222
220
|
name: 'css-hmr',
|
|
@@ -225,10 +223,10 @@ export default async function dev() {
|
|
|
225
223
|
const escaped = JSON.stringify(code);
|
|
226
224
|
return `
|
|
227
225
|
const css = ${escaped};
|
|
228
|
-
const style = document.createElement(
|
|
226
|
+
const style = document.createElement('style');
|
|
229
227
|
style.textContent = css;
|
|
230
228
|
document.head.appendChild(style);
|
|
231
|
-
import.meta.hot
|
|
229
|
+
if (import.meta.hot) import.meta.hot.accept();
|
|
232
230
|
`;
|
|
233
231
|
}
|
|
234
232
|
return code;
|
|
@@ -260,30 +258,31 @@ export default async function dev() {
|
|
|
260
258
|
await analyzeGraph(resolved, seen);
|
|
261
259
|
}
|
|
262
260
|
catch {
|
|
263
|
-
// bare dependency
|
|
261
|
+
// bare dependency
|
|
264
262
|
seen.add(dep);
|
|
265
263
|
}
|
|
266
264
|
}
|
|
267
265
|
}
|
|
268
266
|
catch {
|
|
269
|
-
// ignore unreadable
|
|
267
|
+
// ignore unreadable
|
|
270
268
|
}
|
|
271
269
|
return seen;
|
|
272
270
|
}
|
|
273
|
-
// Prebundle
|
|
271
|
+
// --- Prebundle missing deps (parallel)
|
|
274
272
|
async function prebundleDeps(deps) {
|
|
275
273
|
if (!deps.size)
|
|
276
274
|
return;
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
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.'));
|
|
281
279
|
return;
|
|
280
|
+
}
|
|
282
281
|
console.log(chalk.cyan('📦 Prebundling:'), missing.join(', '));
|
|
283
282
|
await Promise.all(missing.map(async (dep) => {
|
|
284
283
|
try {
|
|
285
284
|
const entryPoint = require.resolve(dep, { paths: [appRoot] });
|
|
286
|
-
const outFile = path.join(cacheDir,
|
|
285
|
+
const outFile = path.join(cacheDir, dep.replace(/\//g, '_') + '.js');
|
|
287
286
|
await esbuild.build({
|
|
288
287
|
entryPoints: [entryPoint],
|
|
289
288
|
bundle: true,
|
|
@@ -300,10 +299,10 @@ export default async function dev() {
|
|
|
300
299
|
}
|
|
301
300
|
}));
|
|
302
301
|
}
|
|
303
|
-
//
|
|
302
|
+
// initial prebundle
|
|
304
303
|
const depsSet = await analyzeGraph(entry);
|
|
305
304
|
await prebundleDeps(depsSet);
|
|
306
|
-
//
|
|
305
|
+
// re-prebundle on package.json changes
|
|
307
306
|
const pkgPath = path.join(appRoot, 'package.json');
|
|
308
307
|
if (await fs.pathExists(pkgPath)) {
|
|
309
308
|
chokidar.watch(pkgPath).on('change', async () => {
|
|
@@ -430,10 +429,10 @@ const overlayId = "__rc_error_overlay__";
|
|
|
430
429
|
if (!url.startsWith('/@source-map'))
|
|
431
430
|
return next();
|
|
432
431
|
try {
|
|
433
|
-
const
|
|
432
|
+
const full = req.url ?? '';
|
|
433
|
+
const parsed = new URL(full, `http://localhost:${port}`);
|
|
434
434
|
const file = parsed.searchParams.get('file') ?? '';
|
|
435
|
-
const
|
|
436
|
-
const lineNum = Number(lineStr) || 0;
|
|
435
|
+
const lineNum = Number(parsed.searchParams.get('line') ?? '0') || 0;
|
|
437
436
|
if (!file) {
|
|
438
437
|
res.writeHead(400);
|
|
439
438
|
return res.end('{}');
|
|
@@ -464,39 +463,45 @@ const overlayId = "__rc_error_overlay__";
|
|
|
464
463
|
res.end(JSON.stringify({ error: err.message }));
|
|
465
464
|
}
|
|
466
465
|
}));
|
|
467
|
-
//
|
|
466
|
+
// Serve /src/* files (transform on the fly)
|
|
468
467
|
app.use((async (req, res, next) => {
|
|
469
468
|
const url = req.url ?? '';
|
|
470
469
|
if (!url.startsWith('/src/') && !url.endsWith('.css'))
|
|
471
470
|
return next();
|
|
472
471
|
const raw = decodeURIComponent((req.url ?? '').split('?')[0]);
|
|
473
|
-
const
|
|
474
|
-
// Try file extensions if not exact file
|
|
472
|
+
const filePathBase = path.join(appRoot, raw.replace(/^\//, ''));
|
|
475
473
|
const exts = ['', '.tsx', '.ts', '.jsx', '.js', '.css'];
|
|
476
474
|
let found = '';
|
|
477
475
|
for (const ext of exts) {
|
|
478
|
-
if (await fs.pathExists(
|
|
479
|
-
found =
|
|
476
|
+
if (await fs.pathExists(filePathBase + ext)) {
|
|
477
|
+
found = filePathBase + ext;
|
|
480
478
|
break;
|
|
481
479
|
}
|
|
482
480
|
}
|
|
483
481
|
if (!found)
|
|
484
482
|
return next();
|
|
485
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
|
+
}
|
|
486
490
|
let code = await fs.readFile(found, 'utf8');
|
|
487
|
-
// rewrite bare imports
|
|
491
|
+
// rewrite bare imports to /@modules/*
|
|
488
492
|
code = code
|
|
489
493
|
.replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
|
|
490
494
|
.replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
|
|
491
|
-
//
|
|
495
|
+
// plugin transforms
|
|
492
496
|
for (const p of plugins) {
|
|
493
497
|
if (p.onTransform) {
|
|
498
|
+
// eslint-disable-next-line no-await-in-loop
|
|
494
499
|
const out = await p.onTransform(code, found);
|
|
495
500
|
if (typeof out === 'string')
|
|
496
501
|
code = out;
|
|
497
502
|
}
|
|
498
503
|
}
|
|
499
|
-
// choose loader by
|
|
504
|
+
// choose loader by ext
|
|
500
505
|
const ext = path.extname(found).toLowerCase();
|
|
501
506
|
const loader = ext === '.ts' ? 'ts' : ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'js';
|
|
502
507
|
const result = await esbuild.transform(code, {
|
|
@@ -514,7 +519,7 @@ const overlayId = "__rc_error_overlay__";
|
|
|
514
519
|
res.end(`// transform error: ${e.message}`);
|
|
515
520
|
}
|
|
516
521
|
}));
|
|
517
|
-
//
|
|
522
|
+
// Serve index.html with HMR + overlay injection
|
|
518
523
|
app.use((async (req, res, next) => {
|
|
519
524
|
const url = req.url ?? '';
|
|
520
525
|
if (url !== '/' && url !== '/index.html')
|
|
@@ -525,7 +530,7 @@ const overlayId = "__rc_error_overlay__";
|
|
|
525
530
|
}
|
|
526
531
|
try {
|
|
527
532
|
let html = await fs.readFile(indexHtml, 'utf8');
|
|
528
|
-
// inject overlay
|
|
533
|
+
// inject runtime overlay and HMR client if not present
|
|
529
534
|
if (!html.includes(RUNTIME_OVERLAY_ROUTE)) {
|
|
530
535
|
html = html.replace('</body>', `\n<script type="module" src="${RUNTIME_OVERLAY_ROUTE}"></script>\n<script type="module">
|
|
531
536
|
const ws = new WebSocket("ws://" + location.host);
|
|
@@ -548,30 +553,29 @@ const overlayId = "__rc_error_overlay__";
|
|
|
548
553
|
res.end(`// html read error: ${err.message}`);
|
|
549
554
|
}
|
|
550
555
|
}));
|
|
551
|
-
//
|
|
556
|
+
// HMR WebSocket
|
|
552
557
|
const server = http.createServer(app);
|
|
553
558
|
const broadcaster = new BroadcastManager(server);
|
|
554
|
-
//
|
|
559
|
+
// Watcher: src
|
|
555
560
|
const watcher = chokidar.watch(path.join(appRoot, 'src'), { ignoreInitial: true });
|
|
556
561
|
watcher.on('change', async (file) => {
|
|
557
562
|
transformCache.delete(file);
|
|
558
|
-
// plugin hook onHotUpdate
|
|
563
|
+
// plugin hook onHotUpdate
|
|
559
564
|
for (const p of plugins) {
|
|
560
565
|
if (p.onHotUpdate) {
|
|
561
566
|
try {
|
|
567
|
+
// eslint-disable-next-line no-await-in-loop
|
|
562
568
|
await p.onHotUpdate(file, {
|
|
563
|
-
|
|
564
|
-
broadcast: (msg) => {
|
|
565
|
-
broadcaster.broadcast(msg);
|
|
566
|
-
},
|
|
569
|
+
broadcast: (m) => broadcaster.broadcast(m),
|
|
567
570
|
});
|
|
568
571
|
}
|
|
569
572
|
catch (err) {
|
|
573
|
+
// plugin errors shouldn't crash server
|
|
574
|
+
// eslint-disable-next-line no-console
|
|
570
575
|
console.warn('plugin onHotUpdate error:', err.message);
|
|
571
576
|
}
|
|
572
577
|
}
|
|
573
578
|
}
|
|
574
|
-
// default: broadcast update for changed file
|
|
575
579
|
broadcaster.broadcast({
|
|
576
580
|
type: 'update',
|
|
577
581
|
path: '/' + path.relative(appRoot, file).replace(/\\/g, '/'),
|
|
@@ -581,20 +585,39 @@ const overlayId = "__rc_error_overlay__";
|
|
|
581
585
|
server.listen(port, async () => {
|
|
582
586
|
const url = `http://localhost:${port}`;
|
|
583
587
|
console.log(chalk.cyan.bold('\n🚀 React Client Dev Server'));
|
|
588
|
+
console.log(chalk.gray('──────────────────────────────'));
|
|
584
589
|
console.log(chalk.green(`⚡ Running at: ${url}`));
|
|
590
|
+
// open if not explicitly disabled
|
|
585
591
|
if (userConfig.server?.open !== false) {
|
|
586
592
|
try {
|
|
587
593
|
await open(url);
|
|
588
594
|
}
|
|
589
595
|
catch {
|
|
590
|
-
|
|
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);
|
|
591
614
|
}
|
|
592
615
|
}
|
|
593
616
|
});
|
|
594
617
|
// graceful shutdown
|
|
595
618
|
process.on('SIGINT', async () => {
|
|
596
619
|
console.log(chalk.red('\n🛑 Shutting down...'));
|
|
597
|
-
|
|
620
|
+
watcher.close();
|
|
598
621
|
broadcaster.close();
|
|
599
622
|
server.close();
|
|
600
623
|
process.exit(0);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
// src/server/broadcastManager.ts
|
|
2
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
2
3
|
import chalk from 'chalk';
|
|
3
4
|
/**
|
|
4
|
-
* BroadcastManager —
|
|
5
|
-
*
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
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 ===
|
|
45
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
31
46
|
ws.send(JSON.stringify(msg));
|
|
32
47
|
}
|
|
33
48
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Close all WebSocket connections and server.
|
|
51
|
+
*/
|
|
37
52
|
close() {
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
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": [
|