react-client 1.0.14 → 1.0.15

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.
@@ -7,24 +7,24 @@ exports.default = dev;
7
7
  const esbuild_1 = __importDefault(require("esbuild"));
8
8
  const connect_1 = __importDefault(require("connect"));
9
9
  const http_1 = __importDefault(require("http"));
10
- const ws_1 = require("ws");
11
10
  const chokidar_1 = __importDefault(require("chokidar"));
12
11
  const detect_port_1 = __importDefault(require("detect-port"));
13
12
  const prompts_1 = __importDefault(require("prompts"));
14
13
  const path_1 = __importDefault(require("path"));
15
14
  const fs_extra_1 = __importDefault(require("fs-extra"));
16
- const loadConfig_1 = require("../../utils/loadConfig");
17
15
  const open_1 = __importDefault(require("open"));
18
16
  const child_process_1 = require("child_process");
19
17
  const chalk_1 = __importDefault(require("chalk"));
18
+ const loadConfig_1 = require("../../utils/loadConfig");
19
+ const broadcastManager_1 = require("../../server/broadcastManager");
20
20
  async function dev() {
21
21
  const root = process.cwd();
22
- // 🧩 Load config
23
22
  const userConfig = await (0, loadConfig_1.loadReactClientConfig)(root);
24
23
  const appRoot = path_1.default.resolve(root, userConfig.root || '.');
25
24
  const defaultPort = userConfig.server?.port || 5173;
26
- const outDir = path_1.default.join(appRoot, userConfig.build?.outDir || '.react-client/dev');
27
- // 🧠 Detect entry (main.tsx / main.jsx)
25
+ const cacheDir = path_1.default.join(appRoot, '.react-client', 'deps');
26
+ await fs_extra_1.default.ensureDir(cacheDir);
27
+ // Detect entry dynamically (main.tsx or 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) {
@@ -32,8 +32,7 @@ async function dev() {
32
32
  process.exit(1);
33
33
  }
34
34
  const indexHtml = path_1.default.join(appRoot, 'index.html');
35
- await fs_extra_1.default.ensureDir(outDir);
36
- // 🧠 Detect available port
35
+ // Detect open port
37
36
  const availablePort = await (0, detect_port_1.default)(defaultPort);
38
37
  const port = availablePort;
39
38
  if (availablePort !== defaultPort) {
@@ -48,122 +47,138 @@ async function dev() {
48
47
  process.exit(0);
49
48
  }
50
49
  }
51
- // ⚔ Auto-install + resolve react-refresh runtime
50
+ // ⚔ React-refresh runtime auto install
52
51
  function safeResolveReactRefresh() {
53
52
  try {
54
53
  return require.resolve('react-refresh/runtime');
55
54
  }
56
55
  catch {
57
- console.warn(chalk_1.default.yellow('āš ļø react-refresh not found — attempting to install...'));
56
+ console.warn(chalk_1.default.yellow('āš ļø react-refresh not found — installing...'));
57
+ (0, child_process_1.execSync)('npm install react-refresh --no-audit --no-fund --silent', {
58
+ cwd: root,
59
+ stdio: 'inherit',
60
+ });
61
+ return require.resolve('react-refresh/runtime');
62
+ }
63
+ }
64
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
65
+ const _reactRefreshRuntime = safeResolveReactRefresh();
66
+ // --- Plugins (core + user)
67
+ const corePlugins = [
68
+ {
69
+ name: 'css-hmr',
70
+ async onTransform(code, id) {
71
+ if (id.endsWith('.css')) {
72
+ const escaped = JSON.stringify(code);
73
+ return `
74
+ const css = ${escaped};
75
+ const style = document.createElement('style');
76
+ style.textContent = css;
77
+ document.head.appendChild(style);
78
+ import.meta.hot && import.meta.hot.accept();
79
+ `;
80
+ }
81
+ return code;
82
+ },
83
+ },
84
+ ];
85
+ const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
86
+ const plugins = [...corePlugins, ...userPlugins];
87
+ // 🧱 Connect app
88
+ const app = (0, connect_1.default)();
89
+ const transformCache = new Map();
90
+ // --- Prebundle persistent deps
91
+ async function prebundleDeps() {
92
+ const pkgFile = path_1.default.join(appRoot, 'package.json');
93
+ if (!fs_extra_1.default.existsSync(pkgFile))
94
+ return;
95
+ const pkg = JSON.parse(await fs_extra_1.default.readFile(pkgFile, 'utf8'));
96
+ const deps = Object.keys(pkg.dependencies || {});
97
+ if (!deps.length)
98
+ return;
99
+ const cached = await fs_extra_1.default.readdir(cacheDir);
100
+ const missing = deps.filter((d) => !cached.includes(d + '.js'));
101
+ if (!missing.length)
102
+ return;
103
+ console.log(chalk_1.default.cyan('šŸ“¦ Prebundling:'), missing.join(', '));
104
+ for (const dep of missing) {
58
105
  try {
59
- (0, child_process_1.execSync)('npm install react-refresh --no-audit --no-fund --silent', {
60
- cwd: root,
61
- stdio: 'inherit',
106
+ const entryPath = require.resolve(dep, { paths: [appRoot] });
107
+ const outFile = path_1.default.join(cacheDir, dep + '.js');
108
+ await esbuild_1.default.build({
109
+ entryPoints: [entryPath],
110
+ bundle: true,
111
+ platform: 'browser',
112
+ format: 'esm',
113
+ outfile: outFile,
114
+ write: true,
115
+ target: 'es2020',
62
116
  });
63
- console.log(chalk_1.default.green('āœ… react-refresh installed successfully.'));
64
- return require.resolve('react-refresh/runtime');
117
+ console.log(chalk_1.default.green(`āœ… Cached ${dep}`));
65
118
  }
66
119
  catch (err) {
67
120
  const msg = err instanceof Error ? err.message : String(err);
68
- console.error(msg);
69
- console.error(chalk_1.default.red('āŒ Failed to install react-refresh automatically.'));
70
- console.error('Please run: npm install react-refresh');
71
- process.exit(1);
121
+ console.warn(chalk_1.default.yellow(`āš ļø Skipped ${dep}: ${msg}`));
72
122
  }
73
123
  }
74
124
  }
75
- const reactRefreshRuntime = safeResolveReactRefresh();
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
91
- const app = (0, connect_1.default)();
92
- // šŸ›” Security headers
93
- app.use((_req, res, next) => {
94
- res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
95
- res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
96
- next();
97
- });
98
- // 1ļøāƒ£ Serve react-refresh runtime with browser shim
99
- app.use('/@react-refresh', async (_req, res) => {
100
- const runtime = await fs_extra_1.default.readFile(reactRefreshRuntime, 'utf8');
101
- const shim = `
102
- window.process = window.process || { env: { NODE_ENV: 'development' } };
103
- window.module = { exports: {} };
104
- window.global = window;
105
- window.require = () => window.module.exports;
106
- `;
107
- res.setHeader('Content-Type', 'application/javascript');
108
- res.end(shim + '\n' + runtime);
109
- });
110
- // 2ļøāƒ£ Serve bare modules dynamically (/@modules/)
125
+ await prebundleDeps();
126
+ // --- Serve prebundled modules
111
127
  app.use('/@modules/', async (req, res, next) => {
112
- let id = req.url?.replace(/^\/@modules\//, '');
128
+ const id = req.url?.replace(/^\/@modules\//, '');
113
129
  if (!id)
114
130
  return next();
115
- id = id.replace(/^\/+/, ''); // normalize
116
131
  try {
117
- const entry = require.resolve(id, { paths: [appRoot] });
118
- const out = await esbuild_1.default.build({
119
- entryPoints: [entry],
132
+ const cacheFile = path_1.default.join(cacheDir, id.replace(/\//g, '_') + '.js');
133
+ if (await fs_extra_1.default.pathExists(cacheFile)) {
134
+ res.setHeader('Content-Type', 'application/javascript');
135
+ return res.end(await fs_extra_1.default.readFile(cacheFile));
136
+ }
137
+ const entryPath = require.resolve(id, { paths: [appRoot] });
138
+ const result = await esbuild_1.default.build({
139
+ entryPoints: [entryPath],
120
140
  bundle: true,
121
- write: false,
122
141
  platform: 'browser',
123
142
  format: 'esm',
124
143
  target: 'es2020',
144
+ write: false,
125
145
  });
146
+ let code = result.outputFiles[0].text;
147
+ if (id === 'react-dom/client') {
148
+ code += `
149
+ import * as ReactDOMClient from '/@modules/react-dom';
150
+ export const createRoot = ReactDOMClient.createRoot || ReactDOMClient.default?.createRoot;
151
+ export default ReactDOMClient.default || ReactDOMClient;
152
+ `;
153
+ }
154
+ await fs_extra_1.default.writeFile(cacheFile, code, 'utf8');
126
155
  res.setHeader('Content-Type', 'application/javascript');
127
- res.end(out.outputFiles[0].text);
156
+ res.end(code);
128
157
  }
129
158
  catch (err) {
130
159
  const msg = err instanceof Error ? err.message : String(err);
131
- console.error(`Failed to resolve module ${id}:`, msg);
132
160
  res.writeHead(500);
133
- res.end(`// Could not resolve module ${id}`);
161
+ res.end(`// Failed to resolve module ${id}: ${msg}`);
134
162
  }
135
163
  });
136
- // 3ļøāƒ£ Serve /src/* files — with caching, deps tracking, and HMR
164
+ // --- Serve /src files dynamically
137
165
  app.use(async (req, res, next) => {
138
- if (!req.url || !req.url.startsWith('/src/'))
166
+ if (!req.url || (!req.url.startsWith('/src/') && !req.url.endsWith('.css')))
167
+ return next();
168
+ const filePath = path_1.default.join(appRoot, decodeURIComponent(req.url.split('?')[0]));
169
+ if (!(await fs_extra_1.default.pathExists(filePath)))
139
170
  return next();
140
171
  try {
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)
145
- return next();
146
- if (transformCache.has(resolvedFile)) {
172
+ if (transformCache.has(filePath)) {
147
173
  res.setHeader('Content-Type', 'application/javascript');
148
- res.end(transformCache.get(resolvedFile));
149
- return;
174
+ return res.end(transformCache.get(filePath));
150
175
  }
151
- let code = await fs_extra_1.default.readFile(resolvedFile, 'utf8');
152
- const ext = path_1.default.extname(resolvedFile).toLowerCase();
153
- // šŸŖ„ Rewrite bare imports → /@modules/
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);
176
+ let code = await fs_extra_1.default.readFile(filePath, 'utf8');
177
+ for (const p of plugins) {
178
+ if (p.onTransform)
179
+ code = await p.onTransform(code, filePath);
166
180
  }
181
+ const ext = path_1.default.extname(filePath);
167
182
  let loader = 'js';
168
183
  if (ext === '.ts')
169
184
  loader = 'ts';
@@ -171,146 +186,96 @@ async function dev() {
171
186
  loader = 'tsx';
172
187
  else if (ext === '.jsx')
173
188
  loader = 'jsx';
174
- const transformed = await esbuild_1.default.transform(code, {
189
+ const result = await esbuild_1.default.transform(code, {
175
190
  loader,
176
191
  sourcemap: 'inline',
177
- sourcefile: req.url,
178
192
  target: 'es2020',
179
- jsxFactory: 'React.createElement',
180
- jsxFragment: 'React.Fragment',
181
193
  });
182
- transformCache.set(resolvedFile, transformed.code);
194
+ transformCache.set(filePath, result.code);
183
195
  res.setHeader('Content-Type', 'application/javascript');
184
- res.end(transformed.code);
196
+ res.end(result.code);
185
197
  }
186
198
  catch (err) {
187
199
  const msg = err instanceof Error ? err.message : String(err);
188
- console.error('Error serving /src file:', msg);
189
200
  res.writeHead(500);
190
201
  res.end(`// Error: ${msg}`);
191
202
  }
192
203
  });
193
- // 4ļøāƒ£ Serve index.html (inject React Refresh + HMR client + overlay)
204
+ // --- Serve index.html with overlay + HMR client
194
205
  app.use(async (req, res, next) => {
195
- if (req.url === '/' || req.url === '/index.html') {
196
- if (!fs_extra_1.default.existsSync(indexHtml)) {
197
- res.writeHead(404);
198
- res.end('index.html not found');
199
- return;
200
- }
201
- let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
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
-
246
- <script type="module">
247
- import "/@react-refresh";
248
- const ws = new WebSocket("ws://" + location.host);
249
- ws.onmessage = async (e) => {
250
- const msg = JSON.parse(e.data);
251
- if (msg.type === "error") {
252
- console.error(msg);
253
- return window.showErrorOverlay?.(msg);
254
- }
255
- if (msg.type === "update") {
256
- try {
257
- await import(msg.path + "?t=" + Date.now());
258
- window.clearErrorOverlay?.();
259
- window.$RefreshRuntime?.performReactRefresh?.();
260
- } catch (err) {
261
- window.showErrorOverlay?.(err);
262
- }
206
+ if (req.url !== '/' && req.url !== '/index.html')
207
+ return next();
208
+ if (!fs_extra_1.default.existsSync(indexHtml)) {
209
+ res.writeHead(404);
210
+ return res.end('index.html not found');
211
+ }
212
+ let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
213
+ html = html.replace('</body>', `
214
+ <script>
215
+ (() => {
216
+ const style = document.createElement('style');
217
+ style.textContent = \`
218
+ .rc-overlay {
219
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
220
+ background: rgba(0,0,0,0.9); color: #ff5555;
221
+ font-family: monospace; padding: 2rem; overflow:auto; z-index: 999999;
263
222
  }
264
- if (msg.type === "reload") location.reload();
223
+ \`;
224
+ document.head.appendChild(style);
225
+ window.showErrorOverlay = (err) => {
226
+ window.clearErrorOverlay?.();
227
+ const el = document.createElement('div');
228
+ el.className = 'rc-overlay';
229
+ el.innerHTML = '<h2>🚨 Error</h2><pre>' + (err.message || err) + '</pre>';
230
+ document.body.appendChild(el);
231
+ window.__overlay = el;
265
232
  };
266
- </script>
267
- </body>`);
268
- res.setHeader('Content-Type', 'text/html');
269
- res.end(html);
270
- }
271
- else
272
- next();
233
+ window.clearErrorOverlay = () => window.__overlay?.remove();
234
+ })();
235
+ </script>
236
+ <script type="module">
237
+ const ws = new WebSocket("ws://" + location.host);
238
+ ws.onmessage = (e) => {
239
+ const msg = JSON.parse(e.data);
240
+ if (msg.type === "reload") location.reload();
241
+ if (msg.type === "error") return window.showErrorOverlay?.(msg);
242
+ if (msg.type === "update") {
243
+ window.clearErrorOverlay?.();
244
+ import(msg.path + "?t=" + Date.now());
245
+ }
246
+ };
247
+ </script>
248
+ </body>`);
249
+ res.setHeader('Content-Type', 'text/html');
250
+ res.end(html);
273
251
  });
274
- // šŸ” HMR with dependency graph
252
+ // --- WebSocket + HMR via BroadcastManager
275
253
  const server = http_1.default.createServer(app);
276
- const wss = new ws_1.WebSocketServer({ server });
277
- const broadcast = (data) => {
278
- const json = JSON.stringify(data);
279
- wss.clients.forEach((c) => c.readyState === 1 && c.send(json));
280
- };
254
+ const broadcaster = new broadcastManager_1.BroadcastManager(server);
281
255
  chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', async (file) => {
282
- console.log(`šŸ”„ File changed: ${file}`);
256
+ console.log(chalk_1.default.yellow(`šŸ”„ Changed: ${file}`));
283
257
  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));
301
- }
258
+ for (const p of plugins) {
259
+ p.onHotUpdate?.(file, {
260
+ broadcast: (msg) => broadcaster.broadcast(msg),
261
+ });
302
262
  }
263
+ broadcaster.broadcast({
264
+ type: 'update',
265
+ path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/'),
266
+ });
303
267
  });
268
+ // šŸš€ Launch
304
269
  server.listen(port, async () => {
305
270
  const url = `http://localhost:${port}`;
306
271
  console.log(chalk_1.default.cyan.bold('\nšŸš€ React Client Dev Server'));
307
- console.log(chalk_1.default.gray('───────────────────────────────'));
272
+ console.log(chalk_1.default.gray('──────────────────────────────'));
308
273
  console.log(chalk_1.default.green(`⚔ Running at: ${url}`));
309
274
  await (0, open_1.default)(url, { newInstance: true });
310
275
  });
311
- process.on('SIGINT', async () => {
276
+ process.on('SIGINT', () => {
312
277
  console.log(chalk_1.default.red('\nšŸ›‘ Shutting down...'));
313
- server.close();
278
+ broadcaster.close();
314
279
  process.exit(0);
315
280
  });
316
281
  }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BroadcastManager = void 0;
7
+ const ws_1 = require("ws");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ /**
10
+ * BroadcastManager — Shared WebSocket utility for dev, preview, and SSR servers.
11
+ * Generic over message type T which defaults to HMRMessage.
12
+ */
13
+ class BroadcastManager {
14
+ constructor(server) {
15
+ this.clients = new Set();
16
+ this.wss = new ws_1.WebSocketServer({ server });
17
+ this.wss.on('connection', (ws) => {
18
+ this.clients.add(ws);
19
+ console.log(chalk_1.default.gray('šŸ”Œ Client connected'));
20
+ ws.on('close', () => {
21
+ this.clients.delete(ws);
22
+ console.log(chalk_1.default.gray('āŽ Client disconnected'));
23
+ });
24
+ ws.on('error', (err) => {
25
+ console.error(chalk_1.default.red('āš ļø WebSocket error:'), err.message);
26
+ });
27
+ });
28
+ }
29
+ broadcast(msg) {
30
+ const data = JSON.stringify(msg);
31
+ for (const client of this.clients) {
32
+ if (client.readyState === ws_1.WebSocket.OPEN) {
33
+ client.send(data);
34
+ }
35
+ }
36
+ }
37
+ send(ws, msg) {
38
+ if (ws.readyState === ws_1.WebSocket.OPEN) {
39
+ ws.send(JSON.stringify(msg));
40
+ }
41
+ }
42
+ getClientCount() {
43
+ return this.clients.size;
44
+ }
45
+ close() {
46
+ console.log(chalk_1.default.red('šŸ›‘ Closing WebSocket connections...'));
47
+ this.wss.close();
48
+ for (const ws of this.clients) {
49
+ try {
50
+ ws.close();
51
+ }
52
+ catch {
53
+ // ignore
54
+ }
55
+ }
56
+ this.clients.clear();
57
+ }
58
+ }
59
+ exports.BroadcastManager = BroadcastManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-client",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
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",