react-client 1.0.21 → 1.0.23

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.
@@ -1,14 +1,4 @@
1
1
  "use strict";
2
- /**
3
- * šŸš€ react-client Dev Server (Final Version)
4
- * Includes:
5
- * - Favicon & public asset support
6
- * - ETag + gzip/brotli caching
7
- * - Persistent prebundle deps (.react-client/deps)
8
- * - HMR + overlay
9
- * - CSS hot reload
10
- * - ESLint + Prettier clean
11
- */
12
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
13
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
14
4
  };
@@ -25,108 +15,52 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
25
15
  const open_1 = __importDefault(require("open"));
26
16
  const child_process_1 = require("child_process");
27
17
  const chalk_1 = __importDefault(require("chalk"));
28
- const zlib_1 = __importDefault(require("zlib"));
29
- const crypto_1 = __importDefault(require("crypto"));
30
18
  const loadConfig_1 = require("../../utils/loadConfig");
31
19
  const broadcastManager_1 = require("../../server/broadcastManager");
32
- // Node polyfill mapping
33
- const NODE_POLYFILLS = {
34
- buffer: 'buffer/',
35
- process: 'process/browser',
36
- path: 'path-browserify',
37
- fs: 'browserify-fs',
38
- os: 'os-browserify/browser',
39
- stream: 'stream-browserify',
40
- util: 'util/',
41
- url: 'url/',
42
- assert: 'assert/',
43
- crypto: 'crypto-browserify',
44
- events: 'events/',
45
- constants: 'constants-browserify',
46
- querystring: 'querystring-es3',
47
- zlib: 'browserify-zlib',
48
- };
49
- // --- Helper utilities
50
- const computeHash = (content) => crypto_1.default.createHash('sha1').update(content).digest('hex');
51
- const getMimeType = (file) => {
52
- const ext = path_1.default.extname(file).toLowerCase();
53
- const mime = {
54
- '.ico': 'image/x-icon',
55
- '.png': 'image/png',
56
- '.jpg': 'image/jpeg',
57
- '.jpeg': 'image/jpeg',
58
- '.gif': 'image/gif',
59
- '.svg': 'image/svg+xml',
60
- '.webp': 'image/webp',
61
- '.json': 'application/json',
62
- '.txt': 'text/plain',
63
- '.js': 'application/javascript',
64
- '.mjs': 'application/javascript',
65
- '.css': 'text/css',
66
- '.html': 'text/html',
67
- '.woff': 'font/woff',
68
- '.woff2': 'font/woff2',
69
- '.ttf': 'font/ttf',
70
- '.otf': 'font/otf',
71
- '.mp4': 'video/mp4',
72
- '.mp3': 'audio/mpeg',
73
- };
74
- return mime[ext] || 'application/octet-stream';
75
- };
76
- // āœ… Unused helpers are underscored to comply with eslint rules
77
- const _gunzipAsync = (input) => new Promise((res, rej) => zlib_1.default.gunzip(input, (e, out) => (e ? rej(e) : res(out))));
78
- const _brotliAsync = (input) => new Promise((res, rej) => zlib_1.default.brotliDecompress(input, (e, out) => (e ? rej(e) : res(out))));
79
20
  async function dev() {
80
21
  const root = process.cwd();
81
22
  const userConfig = await (0, loadConfig_1.loadReactClientConfig)(root);
82
23
  const appRoot = path_1.default.resolve(root, userConfig.root || '.');
83
24
  const defaultPort = userConfig.server?.port || 5173;
84
25
  const cacheDir = path_1.default.join(appRoot, '.react-client', 'deps');
85
- await fs_extra_1.default.ensureDir(cacheDir);
86
- const indexHtml = path_1.default.join(appRoot, 'index.html');
87
26
  const pkgFile = path_1.default.join(appRoot, 'package.json');
88
- // Detect entry
27
+ await fs_extra_1.default.ensureDir(cacheDir);
28
+ // āœ… Detect entry (main.tsx or main.jsx)
89
29
  const possibleEntries = ['src/main.tsx', 'src/main.jsx'];
90
30
  const entry = possibleEntries.map((p) => path_1.default.join(appRoot, p)).find((p) => fs_extra_1.default.existsSync(p));
91
31
  if (!entry) {
92
32
  console.error(chalk_1.default.red('āŒ No entry found: src/main.tsx or src/main.jsx'));
93
33
  process.exit(1);
94
34
  }
95
- // Detect open port
96
- const port = await (0, detect_port_1.default)(defaultPort);
97
- if (port !== defaultPort) {
35
+ const indexHtml = path_1.default.join(appRoot, 'index.html');
36
+ // āœ… Detect open port
37
+ const availablePort = await (0, detect_port_1.default)(defaultPort);
38
+ const port = availablePort;
39
+ if (availablePort !== defaultPort) {
98
40
  const res = await (0, prompts_1.default)({
99
41
  type: 'confirm',
100
42
  name: 'useNewPort',
101
- message: `Port ${defaultPort} is occupied. Use ${port} instead?`,
43
+ message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
102
44
  initial: true,
103
45
  });
104
- if (!res.useNewPort)
46
+ if (!res.useNewPort) {
47
+ console.log('šŸ›‘ Dev server cancelled.');
105
48
  process.exit(0);
49
+ }
106
50
  }
107
- // Ensure react-refresh installed
51
+ // āœ… Ensure react-refresh installed
108
52
  try {
109
53
  require.resolve('react-refresh/runtime');
110
54
  }
111
55
  catch {
112
- console.log(chalk_1.default.yellow('Installing react-refresh...'));
113
- (0, child_process_1.execSync)('npm i react-refresh --silent', { cwd: root, stdio: 'inherit' });
114
- }
115
- // Ensure Node polyfills installed
116
- const missing = Object.keys(NODE_POLYFILLS).filter((m) => {
117
- try {
118
- require.resolve(m, { paths: [appRoot] });
119
- return false;
120
- }
121
- catch {
122
- return true;
123
- }
124
- });
125
- if (missing.length > 0) {
126
- console.log(chalk_1.default.yellow('Installing missing polyfills...'));
127
- (0, child_process_1.execSync)(`npm i ${missing.join(' ')} --silent`, { cwd: appRoot, stdio: 'inherit' });
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
+ console.log(chalk_1.default.green('āœ… react-refresh installed successfully.'));
128
62
  }
129
- // --- Plugins
63
+ // āœ… Core + User Plugins
130
64
  const corePlugins = [
131
65
  {
132
66
  name: 'css-hmr',
@@ -148,26 +82,43 @@ async function dev() {
148
82
  const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
149
83
  const plugins = [...corePlugins, ...userPlugins];
150
84
  const app = (0, connect_1.default)();
151
- const server = http_1.default.createServer(app);
152
- const broadcaster = new broadcastManager_1.BroadcastManager(server);
153
85
  const transformCache = new Map();
154
- // --- Prebundle deps with gzip/brotli caching
155
- async function prebundleDeps() {
156
- if (!(await fs_extra_1.default.pathExists(pkgFile)))
157
- return;
158
- const pkg = JSON.parse(await fs_extra_1.default.readFile(pkgFile, 'utf8'));
159
- const deps = Object.keys(pkg.dependencies || {});
160
- if (!deps.length)
86
+ // āœ… Analyze dependency graph recursively
87
+ async function analyzeGraph(file, seen = new Set()) {
88
+ if (seen.has(file))
89
+ return seen;
90
+ seen.add(file);
91
+ const code = await fs_extra_1.default.readFile(file, 'utf8');
92
+ const matches = [
93
+ ...code.matchAll(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g),
94
+ ...code.matchAll(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g),
95
+ ];
96
+ for (const m of matches) {
97
+ const dep = m[1];
98
+ if (!dep || dep.startsWith('.') || dep.startsWith('/'))
99
+ continue;
100
+ try {
101
+ const resolved = require.resolve(dep, { paths: [appRoot] });
102
+ await analyzeGraph(resolved, seen);
103
+ }
104
+ catch {
105
+ seen.add(dep);
106
+ }
107
+ }
108
+ return seen;
109
+ }
110
+ // āœ… Smart prebundling cache
111
+ async function prebundleDeps(deps) {
112
+ if (!deps.size)
161
113
  return;
162
- const hash = computeHash(JSON.stringify(deps));
163
- const metaFile = path_1.default.join(cacheDir, '_meta.json');
164
- let prevHash = null;
165
- if (await fs_extra_1.default.pathExists(metaFile))
166
- prevHash = (await fs_extra_1.default.readJSON(metaFile)).hash;
167
- if (prevHash === hash)
114
+ const cached = (await fs_extra_1.default.readdir(cacheDir)).map((f) => f.replace('.js', ''));
115
+ const missing = [...deps].filter((d) => !cached.includes(d));
116
+ if (!missing.length) {
117
+ console.log(chalk_1.default.green('āœ… All dependencies already prebundled.'));
168
118
  return;
169
- console.log(chalk_1.default.cyan('šŸ“¦ Rebuilding prebundle cache...'));
170
- await Promise.all(deps.map(async (dep) => {
119
+ }
120
+ console.log(chalk_1.default.cyan('šŸ“¦ Prebundling:'), missing.join(', '));
121
+ await Promise.all(missing.map(async (dep) => {
171
122
  try {
172
123
  const entryPath = require.resolve(dep, { paths: [appRoot] });
173
124
  const outFile = path_1.default.join(cacheDir, dep + '.js');
@@ -176,163 +127,118 @@ async function dev() {
176
127
  bundle: true,
177
128
  platform: 'browser',
178
129
  format: 'esm',
179
- target: 'es2020',
180
130
  outfile: outFile,
181
131
  write: true,
132
+ target: 'es2020',
182
133
  });
183
- const content = await fs_extra_1.default.readFile(outFile);
184
- await fs_extra_1.default.writeFile(outFile + '.gz', zlib_1.default.gzipSync(content));
185
- await fs_extra_1.default.writeFile(outFile + '.br', zlib_1.default.brotliCompressSync(content));
186
134
  console.log(chalk_1.default.green(`āœ… Cached ${dep}`));
187
135
  }
188
- catch (e) {
189
- const err = e;
190
- console.warn(chalk_1.default.yellow(`āš ļø Failed ${dep}: ${err.message}`));
136
+ catch (err) {
137
+ const e = err;
138
+ console.warn(chalk_1.default.yellow(`āš ļø Skipped ${dep}: ${e.message}`));
191
139
  }
192
140
  }));
193
- await fs_extra_1.default.writeJSON(metaFile, { hash });
194
141
  }
195
- await prebundleDeps();
196
- chokidar_1.default.watch(pkgFile).on('change', prebundleDeps);
197
- // --- Serve /@modules/
142
+ // āœ… Initial dependency prebundle
143
+ const deps = await analyzeGraph(entry);
144
+ await prebundleDeps(deps);
145
+ // āœ… Auto re-prebundle when package.json changes
146
+ chokidar_1.default.watch(pkgFile).on('change', async () => {
147
+ console.log(chalk_1.default.yellow('šŸ“¦ package.json changed — rebuilding prebundle cache...'));
148
+ const newDeps = await analyzeGraph(entry);
149
+ await prebundleDeps(newDeps);
150
+ });
151
+ // āœ… Serve /@modules/
198
152
  app.use('/@modules/', async (req, res, next) => {
199
153
  const id = req.url?.replace(/^\/(@modules\/)?/, '');
200
154
  if (!id)
201
155
  return next();
202
- const base = path_1.default.join(cacheDir, id.replace(/\//g, '_') + '.js');
203
- const gz = base + '.gz';
204
- const br = base + '.br';
205
- const accept = req.headers['accept-encoding'] || '';
206
156
  try {
207
- let buf = null;
208
- let encoding = null;
209
- if (/\bbr\b/.test(accept) && (await fs_extra_1.default.pathExists(br))) {
210
- buf = await fs_extra_1.default.readFile(br);
211
- encoding = 'br';
212
- }
213
- else if (/\bgzip\b/.test(accept) && (await fs_extra_1.default.pathExists(gz))) {
214
- buf = await fs_extra_1.default.readFile(gz);
215
- encoding = 'gzip';
216
- }
217
- else if (await fs_extra_1.default.pathExists(base)) {
218
- buf = await fs_extra_1.default.readFile(base);
219
- }
220
- else {
221
- const entryPath = require.resolve(id, { paths: [appRoot] });
222
- const result = await esbuild_1.default.build({
223
- entryPoints: [entryPath],
224
- bundle: true,
225
- platform: 'browser',
226
- format: 'esm',
227
- write: false,
228
- });
229
- buf = Buffer.from(result.outputFiles[0].text);
230
- await fs_extra_1.default.writeFile(base, buf);
231
- }
232
- const etag = `"${computeHash(buf)}"`;
233
- if (req.headers['if-none-match'] === etag) {
234
- res.writeHead(304);
235
- return res.end();
157
+ const cacheFile = path_1.default.join(cacheDir, id.replace(/\//g, '_') + '.js');
158
+ if (await fs_extra_1.default.pathExists(cacheFile)) {
159
+ res.setHeader('Content-Type', 'application/javascript');
160
+ return res.end(await fs_extra_1.default.readFile(cacheFile));
236
161
  }
162
+ const entryPath = require.resolve(id, { paths: [appRoot] });
163
+ const result = await esbuild_1.default.build({
164
+ entryPoints: [entryPath],
165
+ bundle: true,
166
+ platform: 'browser',
167
+ format: 'esm',
168
+ target: 'es2020',
169
+ write: false,
170
+ });
171
+ const code = result.outputFiles[0].text;
172
+ await fs_extra_1.default.writeFile(cacheFile, code, 'utf8');
237
173
  res.setHeader('Content-Type', 'application/javascript');
238
- res.setHeader('ETag', etag);
239
- res.setHeader('Cache-Control', 'no-cache');
240
- if (encoding)
241
- res.setHeader('Content-Encoding', encoding);
242
- res.end(buf);
174
+ res.end(code);
243
175
  }
244
- catch (e) {
245
- const err = e;
176
+ catch (err) {
177
+ const e = err;
178
+ console.error(chalk_1.default.red(`āŒ Failed to load module ${id}: ${e.message}`));
246
179
  res.writeHead(500);
247
- res.end(`// Failed to resolve module ${id}: ${err.message}`);
180
+ res.end(`// Failed to resolve module ${id}: ${e.message}`);
248
181
  }
249
182
  });
250
- // --- Serve /src/ files
183
+ // āœ… Serve /src files dynamically
251
184
  app.use(async (req, res, next) => {
252
185
  if (!req.url || (!req.url.startsWith('/src/') && !req.url.endsWith('.css')))
253
186
  return next();
254
- const filePath = path_1.default.join(appRoot, decodeURIComponent(req.url.split('?')[0]));
187
+ const rawPath = decodeURIComponent(req.url.split('?')[0]);
188
+ let filePath = path_1.default.join(appRoot, rawPath);
189
+ const possibleExts = ['', '.tsx', '.ts', '.jsx', '.js'];
190
+ for (const ext of possibleExts) {
191
+ if (await fs_extra_1.default.pathExists(filePath + ext)) {
192
+ filePath += ext;
193
+ break;
194
+ }
195
+ }
255
196
  if (!(await fs_extra_1.default.pathExists(filePath)))
256
197
  return next();
257
- let code = await fs_extra_1.default.readFile(filePath, 'utf8');
258
- code = code
259
- .replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
260
- .replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
261
- for (const p of plugins)
262
- if (p.onTransform)
263
- code = await p.onTransform(code, filePath);
264
- const loader = filePath.endsWith('.tsx')
265
- ? 'tsx'
266
- : filePath.endsWith('.ts')
267
- ? 'ts'
268
- : filePath.endsWith('.jsx')
269
- ? 'jsx'
270
- : 'js';
271
- const result = await esbuild_1.default.transform(code, { loader, sourcemap: 'inline', target: 'es2020' });
272
- res.setHeader('Content-Type', 'application/javascript');
273
- res.end(result.code);
274
- });
275
- // --- Serve static assets (favicon, /public, etc.)
276
- app.use(async (req, res, next) => {
277
- if (!req.url)
278
- return next();
279
- const assetPath = decodeURIComponent(req.url.split('?')[0]);
280
- const publicDir = path_1.default.join(appRoot, 'public');
281
- const rootFile = path_1.default.join(appRoot, assetPath);
282
- const publicFile = path_1.default.join(publicDir, assetPath);
283
- let targetFile = null;
284
- if (await fs_extra_1.default.pathExists(publicFile))
285
- targetFile = publicFile;
286
- else if (await fs_extra_1.default.pathExists(rootFile))
287
- targetFile = rootFile;
288
- if (!targetFile)
289
- return next();
290
- const stat = await fs_extra_1.default.stat(targetFile);
291
- if (!stat.isFile())
292
- return next();
293
- const etag = `"${stat.size}-${stat.mtimeMs}"`;
294
- if (req.headers['if-none-match'] === etag) {
295
- res.writeHead(304);
296
- return res.end();
198
+ try {
199
+ let code = await fs_extra_1.default.readFile(filePath, 'utf8');
200
+ // Rewrite bare imports → /@modules/*
201
+ code = code
202
+ .replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
203
+ .replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
204
+ for (const p of plugins)
205
+ if (p.onTransform)
206
+ code = await p.onTransform(code, filePath);
207
+ const ext = path_1.default.extname(filePath);
208
+ let loader = 'js';
209
+ if (ext === '.ts')
210
+ loader = 'ts';
211
+ else if (ext === '.tsx')
212
+ loader = 'tsx';
213
+ else if (ext === '.jsx')
214
+ loader = 'jsx';
215
+ const result = await esbuild_1.default.transform(code, {
216
+ loader,
217
+ sourcemap: 'inline',
218
+ target: 'es2020',
219
+ });
220
+ transformCache.set(filePath, result.code);
221
+ res.setHeader('Content-Type', 'application/javascript');
222
+ res.end(result.code);
223
+ }
224
+ catch (err) {
225
+ const e = err;
226
+ console.error(chalk_1.default.red(`āš ļø Transform failed: ${e.message}`));
227
+ res.writeHead(500);
228
+ res.end(`// Error: ${e.message}`);
297
229
  }
298
- res.setHeader('Cache-Control', 'public, max-age=3600');
299
- res.setHeader('ETag', etag);
300
- res.setHeader('Content-Type', getMimeType(targetFile));
301
- fs_extra_1.default.createReadStream(targetFile).pipe(res);
302
230
  });
303
- // --- Serve index.html with overlay + HMR
231
+ // āœ… Serve index.html + overlay + runtime
304
232
  app.use(async (req, res, next) => {
305
233
  if (req.url !== '/' && req.url !== '/index.html')
306
234
  return next();
307
- if (!(await fs_extra_1.default.pathExists(indexHtml))) {
235
+ if (!fs_extra_1.default.existsSync(indexHtml)) {
308
236
  res.writeHead(404);
309
237
  return res.end('index.html not found');
310
238
  }
311
239
  let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
312
240
  html = html.replace('</body>', `
313
- <script>
314
- (() => {
315
- const style = document.createElement('style');
316
- style.textContent = \`
317
- .rc-overlay {
318
- position: fixed; inset: 0;
319
- background: rgba(0,0,0,0.9); color:#fff;
320
- font-family: monospace; padding:2rem; overflow:auto;
321
- z-index:999999; white-space:pre-wrap;
322
- }
323
- \`;
324
- document.head.appendChild(style);
325
- window.showErrorOverlay = (err) => {
326
- window.clearErrorOverlay?.();
327
- const el = document.createElement('div');
328
- el.className = 'rc-overlay';
329
- el.innerHTML = '<h2>🚨 Error</h2><pre>' + (err.message || err) + '</pre>';
330
- document.body.appendChild(el);
331
- window.__overlay = el;
332
- };
333
- window.clearErrorOverlay = () => window.__overlay?.remove();
334
- })();
335
- </script>
241
+ <script type="module" src="/src/runtime/overlay-runtime.js"></script>
336
242
  <script type="module">
337
243
  const ws = new WebSocket("ws://" + location.host);
338
244
  ws.onmessage = (e) => {
@@ -349,21 +255,19 @@ async function dev() {
349
255
  res.setHeader('Content-Type', 'text/html');
350
256
  res.end(html);
351
257
  });
352
- // --- Watchers for HMR + favicon reload
353
- chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', (file) => {
258
+ // āœ… WebSocket + HMR
259
+ const server = http_1.default.createServer(app);
260
+ const broadcaster = new broadcastManager_1.BroadcastManager(server);
261
+ chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', async (file) => {
354
262
  console.log(chalk_1.default.yellow(`šŸ”„ Changed: ${file}`));
355
263
  transformCache.delete(file);
264
+ for (const p of plugins)
265
+ await p.onHotUpdate?.(file, { broadcast: (msg) => broadcaster.broadcast(msg) });
356
266
  broadcaster.broadcast({
357
267
  type: 'update',
358
268
  path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/'),
359
269
  });
360
270
  });
361
- chokidar_1.default
362
- .watch(path_1.default.join(appRoot, 'public', 'favicon.ico'), { ignoreInitial: true })
363
- .on('change', () => {
364
- broadcaster.broadcast({ type: 'reload' });
365
- });
366
- // --- Start server
367
271
  server.listen(port, async () => {
368
272
  const url = `http://localhost:${port}`;
369
273
  console.log(chalk_1.default.cyan.bold('\nšŸš€ React Client Dev Server'));
@@ -4,54 +4,161 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.default = preview;
7
- const connect_1 = __importDefault(require("connect"));
8
- const serve_static_1 = __importDefault(require("serve-static"));
9
7
  const http_1 = __importDefault(require("http"));
10
8
  const path_1 = __importDefault(require("path"));
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const chalk_1 = __importDefault(require("chalk"));
11
11
  const detect_port_1 = __importDefault(require("detect-port"));
12
12
  const prompts_1 = __importDefault(require("prompts"));
13
- const chalk_1 = __importDefault(require("chalk"));
14
13
  const open_1 = __importDefault(require("open"));
15
- const fs_extra_1 = __importDefault(require("fs-extra"));
16
14
  const loadConfig_1 = require("../../utils/loadConfig");
15
+ const MIME = {
16
+ '.html': 'text/html; charset=utf-8',
17
+ '.js': 'application/javascript; charset=utf-8',
18
+ '.mjs': 'application/javascript; charset=utf-8',
19
+ '.json': 'application/json; charset=utf-8',
20
+ '.css': 'text/css; charset=utf-8',
21
+ '.png': 'image/png',
22
+ '.jpg': 'image/jpeg',
23
+ '.jpeg': 'image/jpeg',
24
+ '.gif': 'image/gif',
25
+ '.svg': 'image/svg+xml',
26
+ '.ico': 'image/x-icon',
27
+ '.woff': 'font/woff',
28
+ '.woff2': 'font/woff2',
29
+ '.ttf': 'font/ttf',
30
+ '.map': 'application/octet-stream',
31
+ '.txt': 'text/plain; charset=utf-8',
32
+ };
33
+ function contentType(file) {
34
+ return MIME[path_1.default.extname(file).toLowerCase()] || 'application/octet-stream';
35
+ }
36
+ function setCachingHeaders(res, stat) {
37
+ // Short cache for preview by default, but set ETag/Last-Modified so browsers behave nicely
38
+ const etag = `${stat.size}-${Date.parse(stat.mtime.toString())}`;
39
+ res.setHeader('ETag', etag);
40
+ res.setHeader('Last-Modified', stat.mtime.toUTCString());
41
+ res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate');
42
+ }
17
43
  async function preview() {
18
- const root = process.cwd();
19
- const config = await (0, loadConfig_1.loadReactClientConfig)(root);
20
- const appRoot = path_1.default.resolve(root, config.root || '.');
21
- const outDir = path_1.default.join(appRoot, config.build?.outDir || '.react-client/build');
22
- const defaultPort = config.server?.port || 2202;
23
- if (!fs_extra_1.default.existsSync(outDir)) {
24
- console.error(chalk_1.default.red(`āŒ Build output not found at: ${outDir}`));
25
- console.log(chalk_1.default.gray('Please run `react-client build` first.'));
44
+ const cwd = process.cwd();
45
+ const config = await (0, loadConfig_1.loadReactClientConfig)(cwd);
46
+ const appRoot = path_1.default.resolve(cwd, config.root || '.');
47
+ const outDir = path_1.default.join(appRoot, config.build?.outDir || 'dist');
48
+ const indexHtml = path_1.default.join(outDir, 'index.html');
49
+ if (!(await fs_extra_1.default.pathExists(outDir))) {
50
+ console.error(chalk_1.default.red(`āŒ Preview directory not found: ${outDir}`));
26
51
  process.exit(1);
27
52
  }
28
- const availablePort = await (0, detect_port_1.default)(defaultPort);
29
- const port = availablePort;
30
- if (availablePort !== defaultPort) {
31
- const res = await (0, prompts_1.default)({
53
+ if (!(await fs_extra_1.default.pathExists(indexHtml))) {
54
+ console.warn(chalk_1.default.yellow(`āš ļø index.html not found in ${outDir}. SPA fallback will be disabled.`));
55
+ }
56
+ const defaultPort = config.server?.port || 4173;
57
+ const port = await (0, detect_port_1.default)(defaultPort);
58
+ if (port !== defaultPort) {
59
+ const r = await (0, prompts_1.default)({
32
60
  type: 'confirm',
33
61
  name: 'useNewPort',
34
- message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
35
62
  initial: true,
63
+ message: `Port ${defaultPort} is occupied. Use ${port} instead?`,
36
64
  });
37
- if (!res.useNewPort) {
38
- console.log(chalk_1.default.red('šŸ›‘ Preview server cancelled.'));
65
+ if (!r.useNewPort) {
66
+ console.log('šŸ›‘ Preview cancelled.');
39
67
  process.exit(0);
40
68
  }
41
69
  }
42
- const app = (0, connect_1.default)();
43
- app.use((0, serve_static_1.default)(outDir));
44
- const server = http_1.default.createServer(app);
70
+ const server = http_1.default.createServer(async (req, res) => {
71
+ try {
72
+ const url = req.url || '/';
73
+ // normalize and protect
74
+ const relPath = decodeURIComponent(url.split('?')[0]);
75
+ if (relPath.includes('..')) {
76
+ res.writeHead(400);
77
+ return res.end('Invalid request');
78
+ }
79
+ // handle root -> index.html
80
+ let filePath = path_1.default.join(outDir, relPath);
81
+ const tryIndexFallback = async () => {
82
+ if (await fs_extra_1.default.pathExists(indexHtml)) {
83
+ const stat = await fs_extra_1.default.stat(indexHtml);
84
+ setCachingHeaders(res, stat);
85
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
86
+ return fs_extra_1.default.createReadStream(indexHtml).pipe(res);
87
+ }
88
+ else {
89
+ res.writeHead(404);
90
+ return res.end('Not found');
91
+ }
92
+ };
93
+ // If the request path is a directory, try index.html inside it
94
+ if (relPath.endsWith('/')) {
95
+ const candidate = path_1.default.join(filePath, 'index.html');
96
+ if (await fs_extra_1.default.pathExists(candidate)) {
97
+ filePath = candidate;
98
+ }
99
+ else {
100
+ return tryIndexFallback();
101
+ }
102
+ }
103
+ // If file doesn't exist, fallback to index.html for SPA routes
104
+ if (!(await fs_extra_1.default.pathExists(filePath))) {
105
+ // If request appears to be a static asset (has extension), return 404
106
+ if (path_1.default.extname(filePath)) {
107
+ res.writeHead(404);
108
+ return res.end('Not found');
109
+ }
110
+ return tryIndexFallback();
111
+ }
112
+ const stat = await fs_extra_1.default.stat(filePath);
113
+ if (!stat.isFile()) {
114
+ return tryIndexFallback();
115
+ }
116
+ // Compression/Precompressed support: prefer brotli -> gzip -> raw
117
+ const accept = (req.headers['accept-encoding'] || '');
118
+ const tryPrecompressed = async () => {
119
+ if (accept.includes('br') && (await fs_extra_1.default.pathExists(filePath + '.br'))) {
120
+ res.setHeader('Content-Encoding', 'br');
121
+ res.setHeader('Content-Type', contentType(filePath));
122
+ setCachingHeaders(res, stat);
123
+ return fs_extra_1.default.createReadStream(filePath + '.br').pipe(res);
124
+ }
125
+ if (accept.includes('gzip') && (await fs_extra_1.default.pathExists(filePath + '.gz'))) {
126
+ res.setHeader('Content-Encoding', 'gzip');
127
+ res.setHeader('Content-Type', contentType(filePath));
128
+ setCachingHeaders(res, stat);
129
+ return fs_extra_1.default.createReadStream(filePath + '.gz').pipe(res);
130
+ }
131
+ // default
132
+ res.setHeader('Content-Type', contentType(filePath));
133
+ setCachingHeaders(res, stat);
134
+ return fs_extra_1.default.createReadStream(filePath).pipe(res);
135
+ };
136
+ // ETag / If-None-Match handling
137
+ const etag = `${stat.size}-${Date.parse(stat.mtime.toString())}`;
138
+ const inm = req.headers['if-none-match'];
139
+ if (inm && inm.toString() === etag) {
140
+ res.writeHead(304);
141
+ return res.end();
142
+ }
143
+ return tryPrecompressed();
144
+ }
145
+ catch (err) {
146
+ const e = err;
147
+ console.error('Preview server error:', e);
148
+ res.writeHead(500);
149
+ res.end('Internal Server Error');
150
+ }
151
+ });
45
152
  server.listen(port, async () => {
46
153
  const url = `http://localhost:${port}`;
47
- console.log(chalk_1.default.green(`\n🌐 Preview server running at ${url}`));
48
- if (port !== defaultPort) {
49
- console.log(chalk_1.default.yellow(`āš ļø Using alternate port (default ${defaultPort} was occupied).`));
50
- }
154
+ console.log(chalk_1.default.cyan.bold('\nšŸ”Ž react-client preview'));
155
+ console.log(chalk_1.default.gray('────────────────────────'));
156
+ console.log(chalk_1.default.green(`Serving: ${outDir}`));
157
+ console.log(chalk_1.default.green(`Open: ${url}`));
51
158
  await (0, open_1.default)(url, { newInstance: true });
52
159
  });
53
160
  process.on('SIGINT', () => {
54
- console.log(chalk_1.default.red('\nšŸ›‘ Shutting down preview server...'));
161
+ console.log(chalk_1.default.red('\nšŸ›‘ Shutting down preview...'));
55
162
  server.close();
56
163
  process.exit(0);
57
164
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-client",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
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",