react-client 1.0.13 → 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 user 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
- // āœ… Dynamically detect entry (main.tsx or 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 open 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,117 +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
- // šŸ—ļø 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
92
- const app = (0, connect_1.default)();
93
- // šŸ›” Security headers
94
- app.use((_req, res, next) => {
95
- res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
96
- res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
97
- next();
98
- });
99
- // 🧠 In-memory cache for /@modules
100
- const moduleCache = new Map();
101
- // 1ļøāƒ£ Serve react-refresh runtime with safe browser shim
102
- app.use('/@react-refresh', async (_req, res) => {
103
- const runtime = await fs_extra_1.default.readFile(reactRefreshRuntime, 'utf8');
104
- const shim = `
105
- window.process = window.process || { env: { NODE_ENV: 'development' } };
106
- window.module = { exports: {} };
107
- window.global = window;
108
- window.require = () => window.module.exports;
109
- `;
110
- res.setHeader('Content-Type', 'application/javascript');
111
- res.end(shim + '\n' + runtime);
112
- });
113
- // 2ļøāƒ£ Bare module resolver with memory cache
125
+ await prebundleDeps();
126
+ // --- Serve prebundled modules
114
127
  app.use('/@modules/', async (req, res, next) => {
115
- let id = req.url?.replace(/^\/@modules\//, '');
128
+ const id = req.url?.replace(/^\/@modules\//, '');
116
129
  if (!id)
117
130
  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
- }
127
131
  try {
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
+ }
128
137
  const entryPath = require.resolve(id, { paths: [appRoot] });
129
- const out = await esbuild_1.default.build({
138
+ const result = await esbuild_1.default.build({
130
139
  entryPoints: [entryPath],
131
140
  bundle: true,
132
- write: false,
133
141
  platform: 'browser',
134
142
  format: 'esm',
135
143
  target: 'es2020',
144
+ write: false,
136
145
  });
137
- const code = out.outputFiles[0].text;
138
- moduleCache.set(id, code); // āœ… cache module
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');
139
155
  res.setHeader('Content-Type', 'application/javascript');
140
156
  res.end(code);
141
157
  }
142
158
  catch (err) {
143
159
  const msg = err instanceof Error ? err.message : String(err);
144
- console.error(chalk_1.default.red(`Failed to resolve module ${id}: ${msg}`));
145
160
  res.writeHead(500);
146
- res.end(`// Could not resolve module ${id}`);
161
+ res.end(`// Failed to resolve module ${id}: ${msg}`);
147
162
  }
148
163
  });
149
- // 3ļøāƒ£ Serve /src/* files — on-the-fly transform + bare import rewrite
164
+ // --- Serve /src files dynamically
150
165
  app.use(async (req, res, next) => {
151
- 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)))
152
170
  return next();
153
171
  try {
154
- const filePath = path_1.default.join(appRoot, decodeURIComponent(req.url.split('?')[0]));
155
- if (!(await fs_extra_1.default.pathExists(filePath)))
156
- return next();
172
+ if (transformCache.has(filePath)) {
173
+ res.setHeader('Content-Type', 'application/javascript');
174
+ return res.end(transformCache.get(filePath));
175
+ }
157
176
  let code = await fs_extra_1.default.readFile(filePath, 'utf8');
158
- const ext = path_1.default.extname(filePath).toLowerCase();
159
- // šŸŖ„ 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}"`);
177
+ for (const p of plugins) {
178
+ if (p.onTransform)
179
+ code = await p.onTransform(code, filePath);
180
+ }
181
+ const ext = path_1.default.extname(filePath);
162
182
  let loader = 'js';
163
183
  if (ext === '.ts')
164
184
  loader = 'ts';
@@ -166,104 +186,96 @@ async function dev() {
166
186
  loader = 'tsx';
167
187
  else if (ext === '.jsx')
168
188
  loader = 'jsx';
169
- const transformed = await esbuild_1.default.transform(code, {
189
+ const result = await esbuild_1.default.transform(code, {
170
190
  loader,
171
191
  sourcemap: 'inline',
172
- sourcefile: req.url,
173
192
  target: 'es2020',
174
- jsxFactory: 'React.createElement',
175
- jsxFragment: 'React.Fragment',
176
193
  });
194
+ transformCache.set(filePath, result.code);
177
195
  res.setHeader('Content-Type', 'application/javascript');
178
- res.end(transformed.code);
196
+ res.end(result.code);
179
197
  }
180
198
  catch (err) {
181
199
  const msg = err instanceof Error ? err.message : String(err);
182
- console.error('Error serving /src file:', msg);
183
200
  res.writeHead(500);
184
201
  res.end(`// Error: ${msg}`);
185
202
  }
186
203
  });
187
- // 4ļøāƒ£ Serve index.html with injected refresh + HMR
204
+ // --- Serve index.html with overlay + HMR client
188
205
  app.use(async (req, res, next) => {
189
- if (req.url === '/' || req.url === '/index.html') {
190
- if (!fs_extra_1.default.existsSync(indexHtml)) {
191
- res.writeHead(404);
192
- res.end('index.html not found');
193
- return;
194
- }
195
- let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
196
- html = html.replace('</body>', `
197
- <script type="module">
198
- import "/@react-refresh";
199
- const ws = new WebSocket("ws://" + location.host);
200
- ws.onmessage = async (e) => {
201
- const msg = JSON.parse(e.data);
202
- if (msg.type === "error") {
203
- console.error(msg);
204
- return window.showErrorOverlay?.(msg);
205
- }
206
- if (msg.type === "update") {
207
- try {
208
- await import(msg.path + "?t=" + Date.now());
209
- window.clearErrorOverlay?.();
210
- window.$RefreshRuntime?.performReactRefresh?.();
211
- } catch (err) {
212
- window.showErrorOverlay?.(err);
213
- }
214
- }
215
- if (msg.type === "reload") location.reload();
216
- };
217
- </script>
218
- </body>`);
219
- res.setHeader('Content-Type', 'text/html');
220
- res.end(html);
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');
221
211
  }
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);
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;
228
222
  }
229
- else
230
- next();
231
- }
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;
232
+ };
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);
232
251
  });
233
- // šŸ” HMR WebSocket server
252
+ // --- WebSocket + HMR via BroadcastManager
234
253
  const server = http_1.default.createServer(app);
235
- const wss = new ws_1.WebSocketServer({ server });
236
- const broadcast = (data) => {
237
- const json = JSON.stringify(data);
238
- wss.clients.forEach((c) => c.readyState === 1 && c.send(json));
239
- };
254
+ const broadcaster = new broadcastManager_1.BroadcastManager(server);
240
255
  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) });
252
- }
256
+ console.log(chalk_1.default.yellow(`šŸ”„ Changed: ${file}`));
257
+ transformCache.delete(file);
258
+ for (const p of plugins) {
259
+ p.onHotUpdate?.(file, {
260
+ broadcast: (msg) => broadcaster.broadcast(msg),
261
+ });
253
262
  }
263
+ broadcaster.broadcast({
264
+ type: 'update',
265
+ path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/'),
266
+ });
254
267
  });
255
- // 🟢 Start server
268
+ // šŸš€ Launch
256
269
  server.listen(port, async () => {
257
270
  const url = `http://localhost:${port}`;
258
- console.log(chalk_1.default.cyan.bold(`\nšŸš€ React Client Dev Server`));
259
- console.log(chalk_1.default.gray('───────────────────────────────'));
271
+ console.log(chalk_1.default.cyan.bold('\nšŸš€ React Client Dev Server'));
272
+ console.log(chalk_1.default.gray('──────────────────────────────'));
260
273
  console.log(chalk_1.default.green(`⚔ Running at: ${url}`));
261
274
  await (0, open_1.default)(url, { newInstance: true });
262
275
  });
263
- process.on('SIGINT', async () => {
276
+ process.on('SIGINT', () => {
264
277
  console.log(chalk_1.default.red('\nšŸ›‘ Shutting down...'));
265
- await ctx.dispose();
266
- server.close();
278
+ broadcaster.close();
267
279
  process.exit(0);
268
280
  });
269
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.13",
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",