react-client 1.0.13 → 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.
@@ -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,55 +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
112
  let id = req.url?.replace(/^\/@modules\//, '');
116
113
  if (!id)
117
114
  return next();
118
- // 🧩 Normalize: remove leading slashes that may appear (e.g. "/react")
119
- id = id.replace(/^\/+/, '');
120
- if (!id)
121
- return next();
122
- if (moduleCache.has(id)) {
123
- res.setHeader('Content-Type', 'application/javascript');
124
- res.end(moduleCache.get(id));
125
- return;
126
- }
115
+ id = id.replace(/^\/+/, ''); // normalize
127
116
  try {
128
- const entryPath = require.resolve(id, { paths: [appRoot] });
117
+ const entry = require.resolve(id, { paths: [appRoot] });
129
118
  const out = await esbuild_1.default.build({
130
- entryPoints: [entryPath],
119
+ entryPoints: [entry],
131
120
  bundle: true,
132
121
  write: false,
133
122
  platform: 'browser',
134
123
  format: 'esm',
135
124
  target: 'es2020',
136
125
  });
137
- const code = out.outputFiles[0].text;
138
- moduleCache.set(id, code); // āœ… cache module
139
126
  res.setHeader('Content-Type', 'application/javascript');
140
- res.end(code);
127
+ res.end(out.outputFiles[0].text);
141
128
  }
142
129
  catch (err) {
143
130
  const msg = err instanceof Error ? err.message : String(err);
144
- console.error(chalk_1.default.red(`Failed to resolve module ${id}: ${msg}`));
131
+ console.error(`Failed to resolve module ${id}:`, msg);
145
132
  res.writeHead(500);
146
133
  res.end(`// Could not resolve module ${id}`);
147
134
  }
148
135
  });
149
- // 3ļøāƒ£ Serve /src/* files — on-the-fly transform + bare import rewrite
136
+ // 3ļøāƒ£ Serve /src/* files — with caching, deps tracking, and HMR
150
137
  app.use(async (req, res, next) => {
151
138
  if (!req.url || !req.url.startsWith('/src/'))
152
139
  return next();
153
140
  try {
154
- const filePath = path_1.default.join(appRoot, decodeURIComponent(req.url.split('?')[0]));
155
- 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)
156
145
  return next();
157
- let code = await fs_extra_1.default.readFile(filePath, 'utf8');
158
- 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();
159
153
  // šŸŖ„ Rewrite bare imports → /@modules/
160
- // 🧩 Rewrite *only bare imports* like "react", not "./" or "/" or "../"
161
- code = code.replace(/from\s+['"]((?![\.\/])[a-zA-Z0-9@/_-]+)['"]/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
+ }
162
167
  let loader = 'js';
163
168
  if (ext === '.ts')
164
169
  loader = 'ts';
@@ -174,6 +179,7 @@ async function dev() {
174
179
  jsxFactory: 'React.createElement',
175
180
  jsxFragment: 'React.Fragment',
176
181
  });
182
+ transformCache.set(resolvedFile, transformed.code);
177
183
  res.setHeader('Content-Type', 'application/javascript');
178
184
  res.end(transformed.code);
179
185
  }
@@ -184,7 +190,7 @@ async function dev() {
184
190
  res.end(`// Error: ${msg}`);
185
191
  }
186
192
  });
187
- // 4ļøāƒ£ Serve index.html with injected refresh + HMR
193
+ // 4ļøāƒ£ Serve index.html (inject React Refresh + HMR client + overlay)
188
194
  app.use(async (req, res, next) => {
189
195
  if (req.url === '/' || req.url === '/index.html') {
190
196
  if (!fs_extra_1.default.existsSync(indexHtml)) {
@@ -194,6 +200,49 @@ async function dev() {
194
200
  }
195
201
  let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
196
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
+
197
246
  <script type="module">
198
247
  import "/@react-refresh";
199
248
  const ws = new WebSocket("ws://" + location.host);
@@ -219,18 +268,10 @@ async function dev() {
219
268
  res.setHeader('Content-Type', 'text/html');
220
269
  res.end(html);
221
270
  }
222
- else {
223
- const filePath = path_1.default.join(outDir, req.url || '');
224
- if (await fs_extra_1.default.pathExists(filePath)) {
225
- const content = await fs_extra_1.default.readFile(filePath);
226
- res.setHeader('Content-Type', 'application/javascript');
227
- res.end(content);
228
- }
229
- else
230
- next();
231
- }
271
+ else
272
+ next();
232
273
  });
233
- // šŸ” HMR WebSocket server
274
+ // šŸ” HMR with dependency graph
234
275
  const server = http_1.default.createServer(app);
235
276
  const wss = new ws_1.WebSocketServer({ server });
236
277
  const broadcast = (data) => {
@@ -238,31 +279,37 @@ async function dev() {
238
279
  wss.clients.forEach((c) => c.readyState === 1 && c.send(json));
239
280
  };
240
281
  chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', async (file) => {
241
- try {
242
- console.log(`šŸ”„ Rebuilding: ${file}`);
243
- await ctx.rebuild();
244
- broadcast({ type: 'update', path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/') });
245
- }
246
- catch (err) {
247
- if (err instanceof Error) {
248
- broadcast({ type: 'error', message: err.message, stack: err.stack });
249
- }
250
- else {
251
- 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));
252
301
  }
253
302
  }
254
303
  });
255
- // 🟢 Start server
256
304
  server.listen(port, async () => {
257
305
  const url = `http://localhost:${port}`;
258
- 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'));
259
307
  console.log(chalk_1.default.gray('───────────────────────────────'));
260
308
  console.log(chalk_1.default.green(`⚔ Running at: ${url}`));
261
309
  await (0, open_1.default)(url, { newInstance: true });
262
310
  });
263
311
  process.on('SIGINT', async () => {
264
312
  console.log(chalk_1.default.red('\nšŸ›‘ Shutting down...'));
265
- await ctx.dispose();
266
313
  server.close();
267
314
  process.exit(0);
268
315
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-client",
3
- "version": "1.0.13",
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",