react-client 1.0.14 → 1.0.16

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,143 @@ 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
+ // 🧩 Rewrite bare imports (react, react-dom, etc.) to /@modules/*
178
+ code = code
179
+ .replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_match, dep) => `from "/@modules/${dep}"`)
180
+ .replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_match, dep) => `import("/@modules/${dep}")`);
181
+ // Run plugin transforms
182
+ for (const p of plugins) {
183
+ if (p.onTransform)
184
+ code = await p.onTransform(code, filePath);
166
185
  }
186
+ const ext = path_1.default.extname(filePath);
167
187
  let loader = 'js';
168
188
  if (ext === '.ts')
169
189
  loader = 'ts';
@@ -171,146 +191,96 @@ async function dev() {
171
191
  loader = 'tsx';
172
192
  else if (ext === '.jsx')
173
193
  loader = 'jsx';
174
- const transformed = await esbuild_1.default.transform(code, {
194
+ const result = await esbuild_1.default.transform(code, {
175
195
  loader,
176
196
  sourcemap: 'inline',
177
- sourcefile: req.url,
178
197
  target: 'es2020',
179
- jsxFactory: 'React.createElement',
180
- jsxFragment: 'React.Fragment',
181
198
  });
182
- transformCache.set(resolvedFile, transformed.code);
199
+ transformCache.set(filePath, result.code);
183
200
  res.setHeader('Content-Type', 'application/javascript');
184
- res.end(transformed.code);
201
+ res.end(result.code);
185
202
  }
186
203
  catch (err) {
187
204
  const msg = err instanceof Error ? err.message : String(err);
188
- console.error('Error serving /src file:', msg);
189
205
  res.writeHead(500);
190
206
  res.end(`// Error: ${msg}`);
191
207
  }
192
208
  });
193
- // 4ļøāƒ£ Serve index.html (inject React Refresh + HMR client + overlay)
209
+ // --- Serve index.html with overlay + HMR client
194
210
  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
- }
211
+ if (req.url !== '/' && req.url !== '/index.html')
212
+ return next();
213
+ if (!fs_extra_1.default.existsSync(indexHtml)) {
214
+ res.writeHead(404);
215
+ return res.end('index.html not found');
216
+ }
217
+ let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
218
+ html = html.replace('</body>', `
219
+ <script>
220
+ (() => {
221
+ const style = document.createElement('style');
222
+ style.textContent = \`
223
+ .rc-overlay {
224
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
225
+ background: rgba(0,0,0,0.9); color: #ff5555;
226
+ font-family: monospace; padding: 2rem; overflow:auto; z-index: 999999;
263
227
  }
264
- if (msg.type === "reload") location.reload();
228
+ \`;
229
+ document.head.appendChild(style);
230
+ window.showErrorOverlay = (err) => {
231
+ window.clearErrorOverlay?.();
232
+ const el = document.createElement('div');
233
+ el.className = 'rc-overlay';
234
+ el.innerHTML = '<h2>🚨 Error</h2><pre>' + (err.message || err) + '</pre>';
235
+ document.body.appendChild(el);
236
+ window.__overlay = el;
265
237
  };
266
- </script>
267
- </body>`);
268
- res.setHeader('Content-Type', 'text/html');
269
- res.end(html);
270
- }
271
- else
272
- next();
238
+ window.clearErrorOverlay = () => window.__overlay?.remove();
239
+ })();
240
+ </script>
241
+ <script type="module">
242
+ const ws = new WebSocket("ws://" + location.host);
243
+ ws.onmessage = (e) => {
244
+ const msg = JSON.parse(e.data);
245
+ if (msg.type === "reload") location.reload();
246
+ if (msg.type === "error") return window.showErrorOverlay?.(msg);
247
+ if (msg.type === "update") {
248
+ window.clearErrorOverlay?.();
249
+ import(msg.path + "?t=" + Date.now());
250
+ }
251
+ };
252
+ </script>
253
+ </body>`);
254
+ res.setHeader('Content-Type', 'text/html');
255
+ res.end(html);
273
256
  });
274
- // šŸ” HMR with dependency graph
257
+ // --- WebSocket + HMR via BroadcastManager
275
258
  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
- };
259
+ const broadcaster = new broadcastManager_1.BroadcastManager(server);
281
260
  chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', async (file) => {
282
- console.log(`šŸ”„ File changed: ${file}`);
261
+ console.log(chalk_1.default.yellow(`šŸ”„ Changed: ${file}`));
283
262
  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
- }
263
+ for (const p of plugins) {
264
+ p.onHotUpdate?.(file, {
265
+ broadcast: (msg) => broadcaster.broadcast(msg),
266
+ });
302
267
  }
268
+ broadcaster.broadcast({
269
+ type: 'update',
270
+ path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/'),
271
+ });
303
272
  });
273
+ // šŸš€ Launch
304
274
  server.listen(port, async () => {
305
275
  const url = `http://localhost:${port}`;
306
276
  console.log(chalk_1.default.cyan.bold('\nšŸš€ React Client Dev Server'));
307
- console.log(chalk_1.default.gray('───────────────────────────────'));
277
+ console.log(chalk_1.default.gray('──────────────────────────────'));
308
278
  console.log(chalk_1.default.green(`⚔ Running at: ${url}`));
309
279
  await (0, open_1.default)(url, { newInstance: true });
310
280
  });
311
- process.on('SIGINT', async () => {
281
+ process.on('SIGINT', () => {
312
282
  console.log(chalk_1.default.red('\nšŸ›‘ Shutting down...'));
313
- server.close();
283
+ broadcaster.close();
314
284
  process.exit(0);
315
285
  });
316
286
  }
@@ -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.16",
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",