react-client 1.0.22 → 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,13 +1,15 @@
1
1
  "use strict";
2
2
  /**
3
- * šŸš€ React Client Dev Server — Final Version
4
- * ------------------------------------------
5
- * āœ… Local overlay-runtime.js (Prism + stack mapping)
6
- * āœ… Dynamic /@runtime/overlay-runtime.js alias
7
- * āœ… Automatic HTML injection for overlay + HMR
8
- * āœ… Prebundle cache (.react-client/deps)
9
- * āœ… CSS HMR, relative & bare import handling
10
- * āœ… Favicon & public assets serving
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.
11
13
  */
12
14
  var __importDefault = (this && this.__importDefault) || function (mod) {
13
15
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -23,69 +25,54 @@ const prompts_1 = __importDefault(require("prompts"));
23
25
  const path_1 = __importDefault(require("path"));
24
26
  const fs_extra_1 = __importDefault(require("fs-extra"));
25
27
  const open_1 = __importDefault(require("open"));
26
- const child_process_1 = require("child_process");
27
28
  const chalk_1 = __importDefault(require("chalk"));
28
- const zlib_1 = __importDefault(require("zlib"));
29
- const crypto_1 = __importDefault(require("crypto"));
29
+ const child_process_1 = require("child_process");
30
30
  const loadConfig_1 = require("../../utils/loadConfig");
31
31
  const broadcastManager_1 = require("../../server/broadcastManager");
32
- const computeHash = (content) => crypto_1.default.createHash('sha1').update(content).digest('hex');
33
- const getMimeType = (file) => {
34
- const ext = path_1.default.extname(file).toLowerCase();
35
- const mime = {
36
- '.ico': 'image/x-icon',
37
- '.png': 'image/png',
38
- '.jpg': 'image/jpeg',
39
- '.jpeg': 'image/jpeg',
40
- '.gif': 'image/gif',
41
- '.svg': 'image/svg+xml',
42
- '.webp': 'image/webp',
43
- '.json': 'application/json',
44
- '.txt': 'text/plain',
45
- '.js': 'application/javascript',
46
- '.mjs': 'application/javascript',
47
- '.css': 'text/css',
48
- '.html': 'text/html',
49
- };
50
- return mime[ext] || 'application/octet-stream';
51
- };
32
+ const RUNTIME_OVERLAY = '/src/runtime/overlay-runtime.js';
52
33
  async function dev() {
53
34
  const root = process.cwd();
54
- const userConfig = await (0, loadConfig_1.loadReactClientConfig)(root);
35
+ const userConfig = (await (0, loadConfig_1.loadReactClientConfig)(root));
55
36
  const appRoot = path_1.default.resolve(root, userConfig.root || '.');
56
- const defaultPort = userConfig.server?.port || 5173;
37
+ const defaultPort = userConfig.server?.port ?? 2202;
38
+ // cache dir for prebundled deps
57
39
  const cacheDir = path_1.default.join(appRoot, '.react-client', 'deps');
58
- const pkgFile = path_1.default.join(appRoot, 'package.json');
59
- const indexHtml = path_1.default.join(appRoot, 'index.html');
60
40
  await fs_extra_1.default.ensureDir(cacheDir);
61
- // āœ… Detect entry
62
- const possibleEntries = ['src/main.tsx', 'src/main.jsx'];
63
- 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));
64
44
  if (!entry) {
65
- 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'));
66
46
  process.exit(1);
67
47
  }
68
- // āœ… Detect free port
69
- const port = await (0, detect_port_1.default)(defaultPort);
70
- if (port !== defaultPort) {
71
- const res = await (0, prompts_1.default)({
48
+ const indexHtml = path_1.default.join(appRoot, 'index.html');
49
+ // Select port
50
+ const availablePort = await (0, detect_port_1.default)(defaultPort);
51
+ const port = availablePort;
52
+ if (availablePort !== defaultPort) {
53
+ const response = await (0, prompts_1.default)({
72
54
  type: 'confirm',
73
55
  name: 'useNewPort',
74
- message: `Port ${defaultPort} is occupied. Use ${port} instead?`,
56
+ message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
75
57
  initial: true,
76
58
  });
77
- if (!res.useNewPort)
59
+ if (!response.useNewPort) {
60
+ console.log('šŸ›‘ Dev server cancelled.');
78
61
  process.exit(0);
62
+ }
79
63
  }
80
- // āœ… Ensure react-refresh
64
+ // Ensure react-refresh runtime available (used by many templates)
81
65
  try {
82
66
  require.resolve('react-refresh/runtime');
83
67
  }
84
68
  catch {
85
- console.log(chalk_1.default.yellow('Installing react-refresh...'));
86
- (0, child_process_1.execSync)('npm i react-refresh --silent', { cwd: root, stdio: 'inherit' });
69
+ console.warn(chalk_1.default.yellow('āš ļø react-refresh not found — installing react-refresh...'));
70
+ (0, child_process_1.execSync)('npm install react-refresh --no-audit --no-fund --silent', {
71
+ cwd: appRoot,
72
+ stdio: 'inherit',
73
+ });
87
74
  }
88
- // āœ… Core + user plugins
75
+ // Plugin system (core + user)
89
76
  const corePlugins = [
90
77
  {
91
78
  name: 'css-hmr',
@@ -94,10 +81,10 @@ async function dev() {
94
81
  const escaped = JSON.stringify(code);
95
82
  return `
96
83
  const css = ${escaped};
97
- const style = document.createElement('style');
84
+ const style = document.createElement("style");
98
85
  style.textContent = css;
99
86
  document.head.appendChild(style);
100
- import.meta.hot && import.meta.hot.accept();
87
+ import.meta.hot?.accept();
101
88
  `;
102
89
  }
103
90
  return code;
@@ -106,230 +93,316 @@ async function dev() {
106
93
  ];
107
94
  const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
108
95
  const plugins = [...corePlugins, ...userPlugins];
96
+ // App + caches
109
97
  const app = (0, connect_1.default)();
110
- const server = http_1.default.createServer(app);
111
- const broadcaster = new broadcastManager_1.BroadcastManager(server);
112
98
  const transformCache = new Map();
113
- // 🧱 Persistent prebundle cache
114
- async function prebundleDeps() {
115
- if (!(await fs_extra_1.default.pathExists(pkgFile)))
116
- return;
117
- const pkg = JSON.parse(await fs_extra_1.default.readFile(pkgFile, 'utf8'));
118
- const deps = Object.keys(pkg.dependencies || {});
119
- if (!deps.length)
99
+ // Helper: recursively analyze dependency graph for prebundling (bare imports)
100
+ async function analyzeGraph(file, seen = new Set()) {
101
+ if (seen.has(file))
102
+ return seen;
103
+ seen.add(file);
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
+ }
122
+ }
123
+ }
124
+ catch {
125
+ // ignore unreadable files
126
+ }
127
+ return seen;
128
+ }
129
+ // Prebundle dependencies into cache dir (parallel)
130
+ async function prebundleDeps(deps) {
131
+ if (!deps.size)
120
132
  return;
121
- const hash = computeHash(JSON.stringify(deps));
122
- const metaFile = path_1.default.join(cacheDir, '_meta.json');
123
- let prevHash = null;
124
- if (await fs_extra_1.default.pathExists(metaFile))
125
- prevHash = (await fs_extra_1.default.readJSON(metaFile)).hash;
126
- if (prevHash === hash)
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)
127
137
  return;
128
- console.log(chalk_1.default.cyan('šŸ“¦ Rebuilding prebundle cache...'));
129
- await Promise.all(deps.map(async (dep) => {
138
+ console.log(chalk_1.default.cyan('šŸ“¦ Prebundling:'), missing.join(', '));
139
+ await Promise.all(missing.map(async (dep) => {
130
140
  try {
131
- const entryPath = require.resolve(dep, { paths: [appRoot] });
132
- 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');
133
143
  await esbuild_1.default.build({
134
- entryPoints: [entryPath],
144
+ entryPoints: [entryPoint],
135
145
  bundle: true,
136
146
  platform: 'browser',
137
147
  format: 'esm',
138
- target: 'es2020',
139
148
  outfile: outFile,
140
149
  write: true,
150
+ target: ['es2020'],
141
151
  });
142
- const content = await fs_extra_1.default.readFile(outFile);
143
- await fs_extra_1.default.writeFile(outFile + '.gz', zlib_1.default.gzipSync(content));
144
- await fs_extra_1.default.writeFile(outFile + '.br', zlib_1.default.brotliCompressSync(content));
145
152
  console.log(chalk_1.default.green(`āœ… Cached ${dep}`));
146
153
  }
147
- catch (e) {
148
- const err = e;
154
+ catch (err) {
149
155
  console.warn(chalk_1.default.yellow(`āš ļø Skipped ${dep}: ${err.message}`));
150
156
  }
151
157
  }));
152
- await fs_extra_1.default.writeJSON(metaFile, { hash });
153
158
  }
154
- await prebundleDeps();
155
- chokidar_1.default.watch(pkgFile).on('change', prebundleDeps);
156
- // 🧩 Serve local overlay runtime
157
- app.use('/@runtime/overlay-runtime.js', async (req, res) => {
158
- const overlayPath = path_1.default.join(appRoot, 'src/runtime/overlay-runtime.js');
159
- try {
160
- if (!(await fs_extra_1.default.pathExists(overlayPath))) {
161
- res.writeHead(404);
162
- return res.end(`// Overlay runtime not found: ${overlayPath}`);
163
- }
164
- let code = await fs_extra_1.default.readFile(overlayPath, 'utf8');
165
- // Transform bare imports → /@modules/*
166
- code = code
167
- .replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
168
- .replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
169
- const result = await esbuild_1.default.transform(code, {
170
- loader: 'js',
171
- sourcemap: 'inline',
172
- target: 'es2020',
173
- });
174
- res.setHeader('Content-Type', 'application/javascript');
175
- res.end(result.code);
176
- }
177
- catch (err) {
178
- const e = err;
179
- console.error(chalk_1.default.red(`āŒ Failed to load overlay runtime: ${e.message}`));
180
- res.writeHead(500);
181
- res.end(`// Failed to load overlay runtime: ${e.message}`);
182
- }
183
- });
184
- // 🧠 Serve /@modules/
185
- app.use('/@modules/', async (req, res, next) => {
186
- const id = req.url?.replace(/^\/(@modules\/)?/, '');
187
- 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/'))
188
175
  return next();
176
+ const id = url.replace(/^\/@modules\//, '');
177
+ if (!id) {
178
+ res.writeHead(400);
179
+ return res.end('// invalid module');
180
+ }
189
181
  try {
190
- const cacheFile = path_1.default.join(cacheDir, id.replace(/\//g, '_') + '.js');
182
+ const cacheFile = path_1.default.join(cacheDir, id.replace(/[\\/]/g, '_') + '.js');
191
183
  if (await fs_extra_1.default.pathExists(cacheFile)) {
192
184
  res.setHeader('Content-Type', 'application/javascript');
193
- return res.end(await fs_extra_1.default.readFile(cacheFile));
185
+ res.end(await fs_extra_1.default.readFile(cacheFile, 'utf8'));
186
+ return;
194
187
  }
195
- const entryPath = require.resolve(id, { paths: [appRoot] });
188
+ // Resolve and bundle on-demand
189
+ const entryResolved = require.resolve(id, { paths: [appRoot] });
196
190
  const result = await esbuild_1.default.build({
197
- entryPoints: [entryPath],
191
+ entryPoints: [entryResolved],
198
192
  bundle: true,
193
+ write: false,
199
194
  platform: 'browser',
200
195
  format: 'esm',
201
- target: 'es2020',
202
- write: false,
196
+ target: ['es2020'],
203
197
  });
204
- const code = result.outputFiles[0].text;
205
- 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');
206
201
  res.setHeader('Content-Type', 'application/javascript');
207
- res.end(code);
202
+ res.end(output);
208
203
  }
209
- catch (e) {
210
- const err = e;
211
- console.error(chalk_1.default.red(`āŒ Failed to load module ${id}: ${err.message}`));
204
+ catch (err) {
212
205
  res.writeHead(500);
213
206
  res.end(`// Failed to resolve module ${id}: ${err.message}`);
214
207
  }
215
- });
216
- // 🧩 Serve /src/ and .css files dynamically
217
- app.use(async (req, res, next) => {
218
- 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')
219
213
  return next();
220
- const rawPath = decodeURIComponent(req.url.split('?')[0]);
221
- const filePath = path_1.default.join(appRoot, rawPath);
222
- const possibleExts = ['', '.tsx', '.ts', '.jsx', '.js'];
223
- let resolvedPath = null;
224
- for (const ext of possibleExts) {
225
- const candidate = filePath + ext;
226
- if (await fs_extra_1.default.pathExists(candidate)) {
227
- resolvedPath = candidate;
228
- break;
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'))
226
+ return next();
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('{}');
229
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 }));
230
258
  }
231
- if (!resolvedPath) {
232
- res.writeHead(404);
233
- return res.end(`// File not found: ${filePath}`);
259
+ catch (err) {
260
+ res.writeHead(500);
261
+ res.end(JSON.stringify({ error: err.message }));
234
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) {
275
+ if (await fs_extra_1.default.pathExists(filePath + ext)) {
276
+ found = filePath + ext;
277
+ break;
278
+ }
279
+ }
280
+ if (!found)
281
+ return next();
235
282
  try {
236
- let code = await fs_extra_1.default.readFile(resolvedPath, 'utf8');
237
- // Rewrite bare imports → /@modules/*
283
+ let code = await fs_extra_1.default.readFile(found, 'utf8');
284
+ // rewrite bare imports -> /@modules/<dep>
238
285
  code = code
239
286
  .replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
240
287
  .replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
241
- for (const p of plugins)
242
- if (p.onTransform)
243
- code = await p.onTransform(code, resolvedPath);
244
- const ext = path_1.default.extname(resolvedPath);
245
- let loader = 'js';
246
- if (ext === '.ts')
247
- loader = 'ts';
248
- else if (ext === '.tsx')
249
- loader = 'tsx';
250
- else if (ext === '.jsx')
251
- 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';
252
302
  const result = await esbuild_1.default.transform(code, {
253
303
  loader,
254
304
  sourcemap: 'inline',
255
- target: 'es2020',
305
+ target: ['es2020'],
256
306
  });
307
+ transformCache.set(found, result.code);
257
308
  res.setHeader('Content-Type', 'application/javascript');
258
309
  res.end(result.code);
259
310
  }
260
311
  catch (err) {
261
312
  const e = err;
262
- console.error(chalk_1.default.red(`āš ļø Transform failed: ${e.message}`));
263
313
  res.writeHead(500);
264
- res.end(`// Error: ${e.message}`);
314
+ res.end(`// transform error: ${e.message}`);
265
315
  }
266
- });
267
- // šŸ–¼ļø Serve static assets (favicon + public)
268
- app.use(async (req, res, next) => {
269
- if (!req.url)
270
- return next();
271
- const publicDir = path_1.default.join(appRoot, 'public');
272
- const targetFile = path_1.default.join(publicDir, decodeURIComponent(req.url.split('?')[0]));
273
- if (!(await fs_extra_1.default.pathExists(targetFile)))
274
- return next();
275
- const stat = await fs_extra_1.default.stat(targetFile);
276
- if (!stat.isFile())
277
- return next();
278
- res.setHeader('Content-Type', getMimeType(targetFile));
279
- fs_extra_1.default.createReadStream(targetFile).pipe(res);
280
- });
281
- // 🧩 Serve index.html + overlay + HMR
282
- app.use(async (req, res, next) => {
283
- 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')
284
321
  return next();
285
322
  if (!(await fs_extra_1.default.pathExists(indexHtml))) {
286
323
  res.writeHead(404);
287
324
  return res.end('index.html not found');
288
325
  }
289
- let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
290
- html = html.replace('</body>', `
291
- <script type="module" src="/@runtime/overlay-runtime.js"></script>
292
- <script type="module">
293
- const ws = new WebSocket("ws://" + location.host);
294
- ws.onmessage = (e) => {
295
- const msg = JSON.parse(e.data);
296
- if (msg.type === "reload") location.reload();
297
- if (msg.type === "error") return window.showErrorOverlay?.(msg);
298
- if (msg.type === "update") {
299
- window.clearErrorOverlay?.();
300
- import(msg.path + "?t=" + Date.now());
301
- }
302
- };
303
- </script>
304
- </body>`);
305
- res.setHeader('Content-Type', 'text/html');
306
- res.end(html);
307
- });
308
- // ā™»ļø Watchers
309
- chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', (file) => {
310
- console.log(chalk_1.default.yellow(`šŸ”„ Changed: ${file}`));
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
352
+ const server = http_1.default.createServer(app);
353
+ const broadcaster = new broadcastManager_1.BroadcastManager(server);
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) => {
311
357
  transformCache.delete(file);
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
312
380
  broadcaster.broadcast({
313
381
  type: 'update',
314
382
  path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/'),
315
383
  });
316
384
  });
317
- chokidar_1.default
318
- .watch(path_1.default.join(appRoot, 'src/runtime/overlay-runtime.js'), { ignoreInitial: true })
319
- .on('change', () => {
320
- console.log(chalk_1.default.magenta('ā™»ļø Overlay runtime updated — reloading browser...'));
321
- broadcaster.broadcast({ type: 'reload' });
322
- });
385
+ // start server
323
386
  server.listen(port, async () => {
324
387
  const url = `http://localhost:${port}`;
325
388
  console.log(chalk_1.default.cyan.bold('\nšŸš€ React Client Dev Server'));
326
- console.log(chalk_1.default.gray('──────────────────────────────'));
327
389
  console.log(chalk_1.default.green(`⚔ Running at: ${url}`));
328
- 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
+ }
329
399
  });
330
- process.on('SIGINT', () => {
400
+ // graceful shutdown
401
+ process.on('SIGINT', async () => {
331
402
  console.log(chalk_1.default.red('\nšŸ›‘ Shutting down...'));
403
+ watcher.close();
332
404
  broadcaster.close();
405
+ server.close();
333
406
  process.exit(0);
334
407
  });
335
408
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-client",
3
- "version": "1.0.22",
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",