react-client 1.0.12 → 1.0.14

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/README.md CHANGED
@@ -35,7 +35,7 @@ npm install
35
35
  npm run dev
36
36
  ```
37
37
 
38
- This launches the **custom dev server** — not Vite — built on **Connect + WebSocket + esbuild**, featuring:
38
+ This launches the **custom dev server** — built on **Connect + WebSocket + esbuild**, featuring:
39
39
  - Instant rebuilds
40
40
  - React Fast Refresh (HMR)
41
41
  - Auto port detection & confirmation prompt
@@ -85,9 +85,9 @@ Each template is pre-configured for esbuild, HMR, and fast bootstrapping.
85
85
 
86
86
  ## 💎 Core Features
87
87
 
88
- - ⚡ **Custom Dev Server (no Vite)** — Connect + WebSocket + esbuild
88
+ - ⚡ **Custom Dev Server** — Connect + WebSocket + esbuild
89
89
  - 🔁 **React Fast Refresh (HMR)** — State-preserving reloads
90
- - 💥 **Vite-style Overlay** — Syntax-highlighted stack frames, clickable file links (`vscode://file`)
90
+ - 💥 **Overlay** — Syntax-highlighted stack frames, clickable file links (`vscode://file`)
91
91
  - 🔍 **Source Map Stack Mapping** — Maps runtime errors to original TS/JS source lines
92
92
  - 💬 **Auto Port Detection** — Prompts when default port 5173 is occupied
93
93
  - 🧠 **Smart Config Loader** — Detects project root, compiles `.ts` configs dynamically
@@ -19,12 +19,12 @@ const child_process_1 = require("child_process");
19
19
  const chalk_1 = __importDefault(require("chalk"));
20
20
  async function dev() {
21
21
  const root = process.cwd();
22
- // 🧩 Load user config
22
+ // 🧩 Load config
23
23
  const userConfig = await (0, loadConfig_1.loadReactClientConfig)(root);
24
24
  const appRoot = path_1.default.resolve(root, userConfig.root || '.');
25
25
  const defaultPort = userConfig.server?.port || 5173;
26
26
  const outDir = path_1.default.join(appRoot, userConfig.build?.outDir || '.react-client/dev');
27
- // Dynamically detect entry (main.tsx or main.jsx)
27
+ // 🧠 Detect entry (main.tsx / main.jsx)
28
28
  const possibleEntries = ['src/main.tsx', 'src/main.jsx'];
29
29
  const entry = possibleEntries.map((p) => path_1.default.join(appRoot, p)).find((p) => fs_extra_1.default.existsSync(p));
30
30
  if (!entry) {
@@ -33,7 +33,7 @@ async function dev() {
33
33
  }
34
34
  const indexHtml = path_1.default.join(appRoot, 'index.html');
35
35
  await fs_extra_1.default.ensureDir(outDir);
36
- // ⚙️ Detect open port
36
+ // 🧠 Detect available port
37
37
  const availablePort = await (0, detect_port_1.default)(defaultPort);
38
38
  const port = availablePort;
39
39
  if (availablePort !== defaultPort) {
@@ -73,22 +73,21 @@ async function dev() {
73
73
  }
74
74
  }
75
75
  const reactRefreshRuntime = safeResolveReactRefresh();
76
- // 🏗️ Create esbuild context
77
- const ctx = await esbuild_1.default.context({
78
- entryPoints: [entry],
79
- bundle: true,
80
- sourcemap: true,
81
- outdir: outDir,
82
- define: { 'process.env.NODE_ENV': '"development"' },
83
- loader: { '.ts': 'ts', '.tsx': 'tsx', '.js': 'jsx', '.jsx': 'jsx' },
84
- entryNames: '[name]',
85
- assetNames: 'assets/[name]',
86
- });
87
- await ctx.watch();
88
- console.log(chalk_1.default.gray('📦 Watching and building dev bundle...'));
89
- console.log(chalk_1.default.gray(' Output dir:'), chalk_1.default.blue(outDir));
90
- console.log(chalk_1.default.gray(' Entry file:'), chalk_1.default.yellow(entry));
91
- // 🌐 Connect server setup
76
+ // 🧠 Dependency Graph + Transform Cache
77
+ const deps = new Map(); // dependency → importers
78
+ const transformCache = new Map();
79
+ async function resolveFile(basePath) {
80
+ if (await fs_extra_1.default.pathExists(basePath))
81
+ return basePath;
82
+ const exts = ['.tsx', '.ts', '.jsx', '.js'];
83
+ for (const ext of exts) {
84
+ const candidate = basePath + ext;
85
+ if (await fs_extra_1.default.pathExists(candidate))
86
+ return candidate;
87
+ }
88
+ return null;
89
+ }
90
+ // 🌐 connect server
92
91
  const app = (0, connect_1.default)();
93
92
  // 🛡 Security headers
94
93
  app.use((_req, res, next) => {
@@ -96,9 +95,7 @@ async function dev() {
96
95
  res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
97
96
  next();
98
97
  });
99
- // 🧠 In-memory cache for /@modules
100
- const moduleCache = new Map();
101
- // 1️⃣ Serve react-refresh runtime with safe browser shim
98
+ // 1️⃣ Serve react-refresh runtime with browser shim
102
99
  app.use('/@react-refresh', async (_req, res) => {
103
100
  const runtime = await fs_extra_1.default.readFile(reactRefreshRuntime, 'utf8');
104
101
  const shim = `
@@ -110,50 +107,63 @@ async function dev() {
110
107
  res.setHeader('Content-Type', 'application/javascript');
111
108
  res.end(shim + '\n' + runtime);
112
109
  });
113
- // 2️⃣ Bare module resolver with memory cache
110
+ // 2️⃣ Serve bare modules dynamically (/@modules/)
114
111
  app.use('/@modules/', async (req, res, next) => {
115
- const id = req.url?.replace(/^\/@modules\//, '');
112
+ let id = req.url?.replace(/^\/@modules\//, '');
116
113
  if (!id)
117
114
  return next();
118
- if (moduleCache.has(id)) {
119
- res.setHeader('Content-Type', 'application/javascript');
120
- res.end(moduleCache.get(id));
121
- return;
122
- }
115
+ id = id.replace(/^\/+/, ''); // normalize
123
116
  try {
124
- const entryPath = require.resolve(id, { paths: [appRoot] });
117
+ const entry = require.resolve(id, { paths: [appRoot] });
125
118
  const out = await esbuild_1.default.build({
126
- entryPoints: [entryPath],
119
+ entryPoints: [entry],
127
120
  bundle: true,
128
121
  write: false,
129
122
  platform: 'browser',
130
123
  format: 'esm',
131
124
  target: 'es2020',
132
125
  });
133
- const code = out.outputFiles[0].text;
134
- moduleCache.set(id, code); // ✅ cache module
135
126
  res.setHeader('Content-Type', 'application/javascript');
136
- res.end(code);
127
+ res.end(out.outputFiles[0].text);
137
128
  }
138
129
  catch (err) {
139
130
  const msg = err instanceof Error ? err.message : String(err);
140
- console.error(chalk_1.default.red(`Failed to resolve module ${id}: ${msg}`));
131
+ console.error(`Failed to resolve module ${id}:`, msg);
141
132
  res.writeHead(500);
142
133
  res.end(`// Could not resolve module ${id}`);
143
134
  }
144
135
  });
145
- // 3️⃣ Serve /src/* files — on-the-fly transform + bare import rewrite
136
+ // 3️⃣ Serve /src/* files — with caching, deps tracking, and HMR
146
137
  app.use(async (req, res, next) => {
147
138
  if (!req.url || !req.url.startsWith('/src/'))
148
139
  return next();
149
140
  try {
150
- const filePath = path_1.default.join(appRoot, decodeURIComponent(req.url.split('?')[0]));
151
- if (!(await fs_extra_1.default.pathExists(filePath)))
141
+ const requestPath = decodeURIComponent(req.url.split('?')[0]);
142
+ const filePath = path_1.default.join(appRoot, requestPath);
143
+ const resolvedFile = await resolveFile(filePath);
144
+ if (!resolvedFile)
152
145
  return next();
153
- let code = await fs_extra_1.default.readFile(filePath, 'utf8');
154
- const ext = path_1.default.extname(filePath).toLowerCase();
146
+ if (transformCache.has(resolvedFile)) {
147
+ res.setHeader('Content-Type', 'application/javascript');
148
+ res.end(transformCache.get(resolvedFile));
149
+ return;
150
+ }
151
+ let code = await fs_extra_1.default.readFile(resolvedFile, 'utf8');
152
+ const ext = path_1.default.extname(resolvedFile).toLowerCase();
155
153
  // 🪄 Rewrite bare imports → /@modules/
156
- code = code.replace(/from\s+['"]([^'".\/][^'"]*)['"]/g, (_match, dep) => `from "/@modules/${dep}"`);
154
+ code = code.replace(/from\s+['"]((?![\.\/])[a-zA-Z0-9@/_-]+)['"]/g, (_m, dep) => `from "/@modules/${dep}"`);
155
+ // 🧩 Track dependencies (relative imports)
156
+ const importRegex = /from\s+['"](\.\/[^'"]+|\.{2}\/[^'"]+)['"]/g;
157
+ let match;
158
+ while ((match = importRegex.exec(code)) !== null) {
159
+ const rel = match[1];
160
+ const importer = path_1.default.relative(appRoot, resolvedFile);
161
+ const importedFile = path_1.default.resolve(path_1.default.dirname(resolvedFile), rel);
162
+ const depFile = (await resolveFile(importedFile)) ?? importedFile;
163
+ if (!deps.has(depFile))
164
+ deps.set(depFile, new Set());
165
+ deps.get(depFile).add(importer);
166
+ }
157
167
  let loader = 'js';
158
168
  if (ext === '.ts')
159
169
  loader = 'ts';
@@ -169,6 +179,7 @@ async function dev() {
169
179
  jsxFactory: 'React.createElement',
170
180
  jsxFragment: 'React.Fragment',
171
181
  });
182
+ transformCache.set(resolvedFile, transformed.code);
172
183
  res.setHeader('Content-Type', 'application/javascript');
173
184
  res.end(transformed.code);
174
185
  }
@@ -179,7 +190,7 @@ async function dev() {
179
190
  res.end(`// Error: ${msg}`);
180
191
  }
181
192
  });
182
- // 4️⃣ Serve index.html with injected refresh + HMR
193
+ // 4️⃣ Serve index.html (inject React Refresh + HMR client + overlay)
183
194
  app.use(async (req, res, next) => {
184
195
  if (req.url === '/' || req.url === '/index.html') {
185
196
  if (!fs_extra_1.default.existsSync(indexHtml)) {
@@ -189,6 +200,49 @@ async function dev() {
189
200
  }
190
201
  let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
191
202
  html = html.replace('</body>', `
203
+ <script>
204
+ // 🧩 Lightweight Error Overlay
205
+ (() => {
206
+ const style = document.createElement('style');
207
+ style.textContent = \`
208
+ .rc-overlay {
209
+ position: fixed;
210
+ top: 0; left: 0;
211
+ width: 100vw; height: 100vh;
212
+ background: rgba(0, 0, 0, 0.92);
213
+ color: #ff5555;
214
+ font-family: monospace;
215
+ padding: 2rem;
216
+ overflow: auto;
217
+ z-index: 999999;
218
+ white-space: pre-wrap;
219
+ }
220
+ .rc-overlay h2 {
221
+ color: #ff7575;
222
+ font-size: 1.2rem;
223
+ margin-bottom: 1rem;
224
+ }
225
+ \`;
226
+ document.head.appendChild(style);
227
+
228
+ window.showErrorOverlay = (err) => {
229
+ window.clearErrorOverlay?.();
230
+ const overlay = document.createElement('div');
231
+ overlay.className = 'rc-overlay';
232
+ overlay.innerHTML = '<h2>🚨 React Client Error</h2>' +
233
+ (err.message || err.error || err) + '\\n\\n' + (err.stack || '');
234
+ document.body.appendChild(overlay);
235
+ window.__reactClientOverlay = overlay;
236
+ };
237
+
238
+ window.clearErrorOverlay = () => {
239
+ const overlay = window.__reactClientOverlay;
240
+ if (overlay) overlay.remove();
241
+ window.__reactClientOverlay = null;
242
+ };
243
+ })();
244
+ </script>
245
+
192
246
  <script type="module">
193
247
  import "/@react-refresh";
194
248
  const ws = new WebSocket("ws://" + location.host);
@@ -214,18 +268,10 @@ async function dev() {
214
268
  res.setHeader('Content-Type', 'text/html');
215
269
  res.end(html);
216
270
  }
217
- else {
218
- const filePath = path_1.default.join(outDir, req.url || '');
219
- if (await fs_extra_1.default.pathExists(filePath)) {
220
- const content = await fs_extra_1.default.readFile(filePath);
221
- res.setHeader('Content-Type', 'application/javascript');
222
- res.end(content);
223
- }
224
- else
225
- next();
226
- }
271
+ else
272
+ next();
227
273
  });
228
- // 🔁 HMR WebSocket server
274
+ // 🔁 HMR with dependency graph
229
275
  const server = http_1.default.createServer(app);
230
276
  const wss = new ws_1.WebSocketServer({ server });
231
277
  const broadcast = (data) => {
@@ -233,31 +279,37 @@ async function dev() {
233
279
  wss.clients.forEach((c) => c.readyState === 1 && c.send(json));
234
280
  };
235
281
  chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', async (file) => {
236
- try {
237
- console.log(`🔄 Rebuilding: ${file}`);
238
- await ctx.rebuild();
239
- broadcast({ type: 'update', path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/') });
240
- }
241
- catch (err) {
242
- if (err instanceof Error) {
243
- broadcast({ type: 'error', message: err.message, stack: err.stack });
244
- }
245
- else {
246
- broadcast({ type: 'error', message: String(err) });
282
+ console.log(`🔄 File changed: ${file}`);
283
+ transformCache.delete(file);
284
+ broadcast({ type: 'update', path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/') });
285
+ // Propagate updates to dependents
286
+ const visited = new Set();
287
+ const queue = [file];
288
+ while (queue.length > 0) {
289
+ const dep = queue.pop();
290
+ const importers = deps.get(dep);
291
+ if (!importers)
292
+ continue;
293
+ for (const importer of importers) {
294
+ if (visited.has(importer))
295
+ continue;
296
+ visited.add(importer);
297
+ console.log(chalk_1.default.yellow(`↪️ Updating importer: ${importer}`));
298
+ transformCache.delete(path_1.default.join(appRoot, importer));
299
+ broadcast({ type: 'update', path: '/' + importer.replace(/\\/g, '/') });
300
+ queue.push(path_1.default.join(appRoot, importer));
247
301
  }
248
302
  }
249
303
  });
250
- // 🟢 Start server
251
304
  server.listen(port, async () => {
252
305
  const url = `http://localhost:${port}`;
253
- console.log(chalk_1.default.cyan.bold(`\n🚀 React Client Dev Server`));
306
+ console.log(chalk_1.default.cyan.bold('\n🚀 React Client Dev Server'));
254
307
  console.log(chalk_1.default.gray('───────────────────────────────'));
255
308
  console.log(chalk_1.default.green(`⚡ Running at: ${url}`));
256
309
  await (0, open_1.default)(url, { newInstance: true });
257
310
  });
258
311
  process.on('SIGINT', async () => {
259
312
  console.log(chalk_1.default.red('\n🛑 Shutting down...'));
260
- await ctx.dispose();
261
313
  server.close();
262
314
  process.exit(0);
263
315
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-client",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
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",