react-client 1.0.20 → 1.0.22

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.
package/README.md CHANGED
@@ -59,7 +59,7 @@ import { defineConfig } from 'react-client/config';
59
59
 
60
60
  export default defineConfig({
61
61
  root: './src',
62
- server: { port: 5173 },
62
+ server: { port: 2202 },
63
63
  build: { outDir: '.react-client/build' }
64
64
  });
65
65
  ```
@@ -89,7 +89,7 @@ Each template is pre-configured for esbuild, HMR, and fast bootstrapping.
89
89
  - šŸ” **React Fast Refresh (HMR)** — State-preserving reloads
90
90
  - šŸ’„ **Overlay** — Syntax-highlighted stack frames, clickable file links (`vscode://file`)
91
91
  - šŸ” **Source Map Stack Mapping** — Maps runtime errors to original TS/JS source lines
92
- - šŸ’¬ **Auto Port Detection** — Prompts when default port 5173 is occupied
92
+ - šŸ’¬ **Auto Port Detection** — Prompts when default port 2202 is occupied
93
93
  - 🧠 **Smart Config Loader** — Detects project root, compiles `.ts` configs dynamically
94
94
  - šŸŽØ **PrismJS Highlighting** — For pretty overlay code frames
95
95
  - šŸ”Œ **Plugin Hook System** — Extendable with `configResolved`, `transform`, `buildEnd`
@@ -142,7 +142,7 @@ npm install react-refresh
142
142
  ### āš ļø Port already in use
143
143
  CLI will auto-detect and prompt:
144
144
  ```
145
- Port 5173 is occupied. Use 5174 instead? (Y/n)
145
+ Port 2202 is occupied. Use 5174 instead? (Y/n)
146
146
  ```
147
147
 
148
148
  ### āš ļø Permission denied
@@ -1,4 +1,14 @@
1
1
  "use strict";
2
+ /**
3
+ * šŸš€ React Client Dev Server — Final Version
4
+ * ------------------------------------------
5
+ * āœ… Local overlay-runtime.js (Prism + stack mapping)
6
+ * āœ… Dynamic /@runtime/overlay-runtime.js alias
7
+ * āœ… Automatic HTML injection for overlay + HMR
8
+ * āœ… Prebundle cache (.react-client/deps)
9
+ * āœ… CSS HMR, relative & bare import handling
10
+ * āœ… Favicon & public assets serving
11
+ */
2
12
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
13
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
14
  };
@@ -15,111 +25,67 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
15
25
  const open_1 = __importDefault(require("open"));
16
26
  const child_process_1 = require("child_process");
17
27
  const chalk_1 = __importDefault(require("chalk"));
28
+ const zlib_1 = __importDefault(require("zlib"));
29
+ const crypto_1 = __importDefault(require("crypto"));
18
30
  const loadConfig_1 = require("../../utils/loadConfig");
19
31
  const broadcastManager_1 = require("../../server/broadcastManager");
20
- // 🧠 Browser polyfills for Node built-ins
21
- const NODE_POLYFILLS = {
22
- buffer: 'buffer/',
23
- process: 'process/browser',
24
- path: 'path-browserify',
25
- fs: 'browserify-fs',
26
- os: 'os-browserify/browser',
27
- stream: 'stream-browserify',
28
- util: 'util/',
29
- url: 'url/',
30
- assert: 'assert/',
31
- crypto: 'crypto-browserify',
32
- events: 'events/',
33
- constants: 'constants-browserify',
34
- querystring: 'querystring-es3',
35
- zlib: 'browserify-zlib',
32
+ const computeHash = (content) => crypto_1.default.createHash('sha1').update(content).digest('hex');
33
+ const getMimeType = (file) => {
34
+ const ext = path_1.default.extname(file).toLowerCase();
35
+ const mime = {
36
+ '.ico': 'image/x-icon',
37
+ '.png': 'image/png',
38
+ '.jpg': 'image/jpeg',
39
+ '.jpeg': 'image/jpeg',
40
+ '.gif': 'image/gif',
41
+ '.svg': 'image/svg+xml',
42
+ '.webp': 'image/webp',
43
+ '.json': 'application/json',
44
+ '.txt': 'text/plain',
45
+ '.js': 'application/javascript',
46
+ '.mjs': 'application/javascript',
47
+ '.css': 'text/css',
48
+ '.html': 'text/html',
49
+ };
50
+ return mime[ext] || 'application/octet-stream';
36
51
  };
37
- // List of NPM packages required for polyfills
38
- const POLYFILL_PACKAGES = [
39
- 'buffer',
40
- 'process',
41
- 'path-browserify',
42
- 'browserify-fs',
43
- 'os-browserify',
44
- 'stream-browserify',
45
- 'util',
46
- 'url',
47
- 'assert',
48
- 'crypto-browserify',
49
- 'events',
50
- 'constants-browserify',
51
- 'querystring-es3',
52
- 'browserify-zlib',
53
- ];
54
52
  async function dev() {
55
53
  const root = process.cwd();
56
54
  const userConfig = await (0, loadConfig_1.loadReactClientConfig)(root);
57
55
  const appRoot = path_1.default.resolve(root, userConfig.root || '.');
58
56
  const defaultPort = userConfig.server?.port || 5173;
59
57
  const cacheDir = path_1.default.join(appRoot, '.react-client', 'deps');
58
+ const pkgFile = path_1.default.join(appRoot, 'package.json');
59
+ const indexHtml = path_1.default.join(appRoot, 'index.html');
60
60
  await fs_extra_1.default.ensureDir(cacheDir);
61
- // Detect entry file
61
+ // āœ… Detect entry
62
62
  const possibleEntries = ['src/main.tsx', 'src/main.jsx'];
63
63
  const entry = possibleEntries.map((p) => path_1.default.join(appRoot, p)).find((p) => fs_extra_1.default.existsSync(p));
64
64
  if (!entry) {
65
65
  console.error(chalk_1.default.red('āŒ No entry found: src/main.tsx or src/main.jsx'));
66
66
  process.exit(1);
67
67
  }
68
- const indexHtml = path_1.default.join(appRoot, 'index.html');
69
- // Detect available port
70
- const availablePort = await (0, detect_port_1.default)(defaultPort);
71
- const port = availablePort;
72
- if (availablePort !== defaultPort) {
68
+ // āœ… Detect free port
69
+ const port = await (0, detect_port_1.default)(defaultPort);
70
+ if (port !== defaultPort) {
73
71
  const res = await (0, prompts_1.default)({
74
72
  type: 'confirm',
75
73
  name: 'useNewPort',
76
- message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
74
+ message: `Port ${defaultPort} is occupied. Use ${port} instead?`,
77
75
  initial: true,
78
76
  });
79
- if (!res.useNewPort) {
80
- console.log('šŸ›‘ Dev server cancelled.');
77
+ if (!res.useNewPort)
81
78
  process.exit(0);
82
- }
83
79
  }
84
- // 🧩 Auto-install react-refresh
80
+ // āœ… Ensure react-refresh
85
81
  try {
86
82
  require.resolve('react-refresh/runtime');
87
83
  }
88
84
  catch {
89
- console.warn(chalk_1.default.yellow('āš ļø react-refresh not found — installing...'));
90
- (0, child_process_1.execSync)('npm install react-refresh --no-audit --no-fund --silent', {
91
- cwd: root,
92
- stdio: 'inherit',
93
- });
94
- console.log(chalk_1.default.green('āœ… react-refresh installed successfully.'));
95
- }
96
- // 🧩 Auto-install missing polyfill packages
97
- const missingPolyfills = POLYFILL_PACKAGES.filter((pkg) => {
98
- try {
99
- require.resolve(pkg, { paths: [appRoot] });
100
- return false;
101
- }
102
- catch {
103
- return true;
104
- }
105
- });
106
- if (missingPolyfills.length > 0) {
107
- console.log(chalk_1.default.yellow('āš™ļø Installing missing polyfill packages...'));
108
- console.log(chalk_1.default.gray('šŸ“¦ ' + missingPolyfills.join(', ')));
109
- try {
110
- (0, child_process_1.execSync)(`npm install ${missingPolyfills.join(' ')} --no-audit --no-fund --silent`, {
111
- cwd: appRoot,
112
- stdio: 'inherit',
113
- });
114
- console.log(chalk_1.default.green('āœ… Polyfills installed successfully.'));
115
- }
116
- catch (err) {
117
- console.error(chalk_1.default.red('āŒ Failed to install polyfills automatically.'));
118
- console.error(err);
119
- process.exit(1);
120
- }
85
+ console.log(chalk_1.default.yellow('Installing react-refresh...'));
86
+ (0, child_process_1.execSync)('npm i react-refresh --silent', { cwd: root, stdio: 'inherit' });
121
87
  }
122
- // --- Plugins
88
+ // āœ… Core + user plugins
123
89
  const corePlugins = [
124
90
  {
125
91
  name: 'css-hmr',
@@ -141,109 +107,141 @@ async function dev() {
141
107
  const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
142
108
  const plugins = [...corePlugins, ...userPlugins];
143
109
  const app = (0, connect_1.default)();
110
+ const server = http_1.default.createServer(app);
111
+ const broadcaster = new broadcastManager_1.BroadcastManager(server);
144
112
  const transformCache = new Map();
145
- // 🧱 Polyfilled module builder
146
- async function buildModuleWithSafeWrapper(id) {
147
- const cacheFile = path_1.default.join(cacheDir, id.replace(/\//g, '_') + '.js');
148
- if (await fs_extra_1.default.pathExists(cacheFile))
149
- return fs_extra_1.default.readFile(cacheFile, 'utf8');
150
- // 🧠 Polyfill detection
151
- const polyId = NODE_POLYFILLS[id];
152
- if (polyId) {
153
- console.log(chalk_1.default.gray(`🧩 Using polyfill for ${id}: ${polyId}`));
154
- const result = await esbuild_1.default.build({
155
- entryPoints: [require.resolve(polyId, { paths: [appRoot] })],
156
- bundle: true,
157
- platform: 'browser',
158
- format: 'esm',
159
- target: 'es2020',
160
- write: false,
161
- });
162
- const polyCode = result.outputFiles[0].text;
163
- await fs_extra_1.default.writeFile(cacheFile, polyCode, 'utf8');
164
- return polyCode;
165
- }
166
- // 🧱 Normal dependency
167
- let entryPath = null;
168
- try {
169
- entryPath = require.resolve(id, { paths: [appRoot] });
170
- }
171
- catch {
172
- const base = id.split('/')[0];
113
+ // 🧱 Persistent prebundle cache
114
+ async function prebundleDeps() {
115
+ if (!(await fs_extra_1.default.pathExists(pkgFile)))
116
+ return;
117
+ const pkg = JSON.parse(await fs_extra_1.default.readFile(pkgFile, 'utf8'));
118
+ const deps = Object.keys(pkg.dependencies || {});
119
+ if (!deps.length)
120
+ return;
121
+ const hash = computeHash(JSON.stringify(deps));
122
+ const metaFile = path_1.default.join(cacheDir, '_meta.json');
123
+ let prevHash = null;
124
+ if (await fs_extra_1.default.pathExists(metaFile))
125
+ prevHash = (await fs_extra_1.default.readJSON(metaFile)).hash;
126
+ if (prevHash === hash)
127
+ return;
128
+ console.log(chalk_1.default.cyan('šŸ“¦ Rebuilding prebundle cache...'));
129
+ await Promise.all(deps.map(async (dep) => {
173
130
  try {
174
- entryPath = require.resolve(base, { paths: [appRoot] });
131
+ const entryPath = require.resolve(dep, { paths: [appRoot] });
132
+ const outFile = path_1.default.join(cacheDir, dep + '.js');
133
+ await esbuild_1.default.build({
134
+ entryPoints: [entryPath],
135
+ bundle: true,
136
+ platform: 'browser',
137
+ format: 'esm',
138
+ target: 'es2020',
139
+ outfile: outFile,
140
+ write: true,
141
+ });
142
+ const content = await fs_extra_1.default.readFile(outFile);
143
+ await fs_extra_1.default.writeFile(outFile + '.gz', zlib_1.default.gzipSync(content));
144
+ await fs_extra_1.default.writeFile(outFile + '.br', zlib_1.default.brotliCompressSync(content));
145
+ console.log(chalk_1.default.green(`āœ… Cached ${dep}`));
146
+ }
147
+ catch (e) {
148
+ const err = e;
149
+ console.warn(chalk_1.default.yellow(`āš ļø Skipped ${dep}: ${err.message}`));
175
150
  }
176
- catch {
177
- entryPath = null;
151
+ }));
152
+ await fs_extra_1.default.writeJSON(metaFile, { hash });
153
+ }
154
+ await prebundleDeps();
155
+ chokidar_1.default.watch(pkgFile).on('change', prebundleDeps);
156
+ // 🧩 Serve local overlay runtime
157
+ app.use('/@runtime/overlay-runtime.js', async (req, res) => {
158
+ const overlayPath = path_1.default.join(appRoot, 'src/runtime/overlay-runtime.js');
159
+ try {
160
+ if (!(await fs_extra_1.default.pathExists(overlayPath))) {
161
+ res.writeHead(404);
162
+ return res.end(`// Overlay runtime not found: ${overlayPath}`);
178
163
  }
164
+ let code = await fs_extra_1.default.readFile(overlayPath, 'utf8');
165
+ // Transform bare imports → /@modules/*
166
+ code = code
167
+ .replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
168
+ .replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
169
+ const result = await esbuild_1.default.transform(code, {
170
+ loader: 'js',
171
+ sourcemap: 'inline',
172
+ target: 'es2020',
173
+ });
174
+ res.setHeader('Content-Type', 'application/javascript');
175
+ res.end(result.code);
179
176
  }
180
- if (!entryPath)
181
- throw new Error(`Module ${id} not found (resolve failed)`);
182
- const result = await esbuild_1.default.build({
183
- entryPoints: [entryPath],
184
- bundle: true,
185
- platform: 'browser',
186
- format: 'esm',
187
- target: 'es2020',
188
- write: false,
189
- });
190
- const originalCode = result.outputFiles[0].text;
191
- const isSubpath = id.includes('/');
192
- let finalCode = originalCode;
193
- if (isSubpath) {
194
- const base = id.split('/')[0];
195
- finalCode += `
196
- // --- react-client auto wrapper for subpath: ${id}
197
- import * as __base from '/@modules/${base}';
198
- export const __rc_dynamic = __base;
199
- export default __base.default || __base;
200
- `;
177
+ catch (err) {
178
+ const e = err;
179
+ console.error(chalk_1.default.red(`āŒ Failed to load overlay runtime: ${e.message}`));
180
+ res.writeHead(500);
181
+ res.end(`// Failed to load overlay runtime: ${e.message}`);
201
182
  }
202
- await fs_extra_1.default.writeFile(cacheFile, finalCode, 'utf8');
203
- return finalCode;
204
- }
205
- // --- /@modules/
183
+ });
184
+ // 🧠 Serve /@modules/
206
185
  app.use('/@modules/', async (req, res, next) => {
207
186
  const id = req.url?.replace(/^\/(@modules\/)?/, '');
208
187
  if (!id)
209
188
  return next();
210
189
  try {
211
- const code = await buildModuleWithSafeWrapper(id);
190
+ const cacheFile = path_1.default.join(cacheDir, id.replace(/\//g, '_') + '.js');
191
+ if (await fs_extra_1.default.pathExists(cacheFile)) {
192
+ res.setHeader('Content-Type', 'application/javascript');
193
+ return res.end(await fs_extra_1.default.readFile(cacheFile));
194
+ }
195
+ const entryPath = require.resolve(id, { paths: [appRoot] });
196
+ const result = await esbuild_1.default.build({
197
+ entryPoints: [entryPath],
198
+ bundle: true,
199
+ platform: 'browser',
200
+ format: 'esm',
201
+ target: 'es2020',
202
+ write: false,
203
+ });
204
+ const code = result.outputFiles[0].text;
205
+ await fs_extra_1.default.writeFile(cacheFile, code, 'utf8');
212
206
  res.setHeader('Content-Type', 'application/javascript');
213
207
  res.end(code);
214
208
  }
215
- catch (err) {
216
- const e = err;
217
- console.error(chalk_1.default.red(`āŒ Failed to load module ${id}: ${e.message}`));
209
+ catch (e) {
210
+ const err = e;
211
+ console.error(chalk_1.default.red(`āŒ Failed to load module ${id}: ${err.message}`));
218
212
  res.writeHead(500);
219
- res.end(`// Failed to resolve module ${id}: ${e.message}`);
213
+ res.end(`// Failed to resolve module ${id}: ${err.message}`);
220
214
  }
221
215
  });
222
- // --- Universal transform for all project files
216
+ // 🧩 Serve /src/ and .css files dynamically
223
217
  app.use(async (req, res, next) => {
224
- const urlPath = decodeURIComponent(req.url.split('?')[0]);
225
- if (urlPath.includes('node_modules'))
218
+ if (!req.url || (!req.url.startsWith('/src/') && !req.url.endsWith('.css')))
226
219
  return next();
227
- let filePath = path_1.default.join(appRoot, urlPath);
228
- const possibleExts = ['', '.tsx', '.ts', '.jsx', '.js', '.css'];
220
+ const rawPath = decodeURIComponent(req.url.split('?')[0]);
221
+ const filePath = path_1.default.join(appRoot, rawPath);
222
+ const possibleExts = ['', '.tsx', '.ts', '.jsx', '.js'];
223
+ let resolvedPath = null;
229
224
  for (const ext of possibleExts) {
230
- if (await fs_extra_1.default.pathExists(filePath + ext)) {
231
- filePath += ext;
225
+ const candidate = filePath + ext;
226
+ if (await fs_extra_1.default.pathExists(candidate)) {
227
+ resolvedPath = candidate;
232
228
  break;
233
229
  }
234
230
  }
235
- if (!(await fs_extra_1.default.pathExists(filePath)))
236
- return next();
231
+ if (!resolvedPath) {
232
+ res.writeHead(404);
233
+ return res.end(`// File not found: ${filePath}`);
234
+ }
237
235
  try {
238
- let code = await fs_extra_1.default.readFile(filePath, 'utf8');
239
- // Rewrite bare imports
236
+ let code = await fs_extra_1.default.readFile(resolvedPath, 'utf8');
237
+ // Rewrite bare imports → /@modules/*
240
238
  code = code
241
239
  .replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
242
240
  .replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
243
241
  for (const p of plugins)
244
242
  if (p.onTransform)
245
- code = await p.onTransform(code, filePath);
246
- const ext = path_1.default.extname(filePath);
243
+ code = await p.onTransform(code, resolvedPath);
244
+ const ext = path_1.default.extname(resolvedPath);
247
245
  let loader = 'js';
248
246
  if (ext === '.ts')
249
247
  loader = 'ts';
@@ -256,7 +254,6 @@ async function dev() {
256
254
  sourcemap: 'inline',
257
255
  target: 'es2020',
258
256
  });
259
- transformCache.set(filePath, result.code);
260
257
  res.setHeader('Content-Type', 'application/javascript');
261
258
  res.end(result.code);
262
259
  }
@@ -267,38 +264,31 @@ async function dev() {
267
264
  res.end(`// Error: ${e.message}`);
268
265
  }
269
266
  });
270
- // --- index.html + overlay + HMR
267
+ // šŸ–¼ļø Serve static assets (favicon + public)
268
+ app.use(async (req, res, next) => {
269
+ if (!req.url)
270
+ return next();
271
+ const publicDir = path_1.default.join(appRoot, 'public');
272
+ const targetFile = path_1.default.join(publicDir, decodeURIComponent(req.url.split('?')[0]));
273
+ if (!(await fs_extra_1.default.pathExists(targetFile)))
274
+ return next();
275
+ const stat = await fs_extra_1.default.stat(targetFile);
276
+ if (!stat.isFile())
277
+ return next();
278
+ res.setHeader('Content-Type', getMimeType(targetFile));
279
+ fs_extra_1.default.createReadStream(targetFile).pipe(res);
280
+ });
281
+ // 🧩 Serve index.html + overlay + HMR
271
282
  app.use(async (req, res, next) => {
272
283
  if (req.url !== '/' && req.url !== '/index.html')
273
284
  return next();
274
- if (!fs_extra_1.default.existsSync(indexHtml)) {
285
+ if (!(await fs_extra_1.default.pathExists(indexHtml))) {
275
286
  res.writeHead(404);
276
287
  return res.end('index.html not found');
277
288
  }
278
289
  let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
279
290
  html = html.replace('</body>', `
280
- <script>
281
- (() => {
282
- const style = document.createElement('style');
283
- style.textContent = \`
284
- .rc-overlay {
285
- position: fixed; inset: 0; background: rgba(0,0,0,0.9);
286
- color: #ff5555; font-family: monospace;
287
- padding: 2rem; overflow:auto; z-index: 999999;
288
- }
289
- \`;
290
- document.head.appendChild(style);
291
- window.showErrorOverlay = (err) => {
292
- window.clearErrorOverlay?.();
293
- const el = document.createElement('div');
294
- el.className = 'rc-overlay';
295
- el.innerHTML = '<h2>🚨 Error</h2><pre>' + (err.message || err) + '</pre>';
296
- document.body.appendChild(el);
297
- window.__overlay = el;
298
- };
299
- window.clearErrorOverlay = () => window.__overlay?.remove();
300
- })();
301
- </script>
291
+ <script type="module" src="/@runtime/overlay-runtime.js"></script>
302
292
  <script type="module">
303
293
  const ws = new WebSocket("ws://" + location.host);
304
294
  ws.onmessage = (e) => {
@@ -315,28 +305,26 @@ async function dev() {
315
305
  res.setHeader('Content-Type', 'text/html');
316
306
  res.end(html);
317
307
  });
318
- // --- WebSocket + HMR
319
- const server = http_1.default.createServer(app);
320
- const broadcaster = new broadcastManager_1.BroadcastManager(server);
321
- chokidar_1.default.watch(appRoot, { ignoreInitial: true }).on('change', async (file) => {
322
- if (file.includes('node_modules') || file.includes('.react-client'))
323
- return;
308
+ // ā™»ļø Watchers
309
+ chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', (file) => {
324
310
  console.log(chalk_1.default.yellow(`šŸ”„ Changed: ${file}`));
325
311
  transformCache.delete(file);
326
- for (const p of plugins)
327
- await p.onHotUpdate?.(file, { broadcast: (msg) => broadcaster.broadcast(msg) });
328
312
  broadcaster.broadcast({
329
313
  type: 'update',
330
314
  path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/'),
331
315
  });
332
316
  });
317
+ chokidar_1.default
318
+ .watch(path_1.default.join(appRoot, 'src/runtime/overlay-runtime.js'), { ignoreInitial: true })
319
+ .on('change', () => {
320
+ console.log(chalk_1.default.magenta('ā™»ļø Overlay runtime updated — reloading browser...'));
321
+ broadcaster.broadcast({ type: 'reload' });
322
+ });
333
323
  server.listen(port, async () => {
334
324
  const url = `http://localhost:${port}`;
335
325
  console.log(chalk_1.default.cyan.bold('\nšŸš€ React Client Dev Server'));
336
326
  console.log(chalk_1.default.gray('──────────────────────────────'));
337
327
  console.log(chalk_1.default.green(`⚔ Running at: ${url}`));
338
- if (port !== defaultPort)
339
- console.log(chalk_1.default.yellow(`āš ļø Using alternate port (default ${defaultPort} occupied)`));
340
328
  await (0, open_1.default)(url, { newInstance: true });
341
329
  });
342
330
  process.on('SIGINT', () => {
@@ -51,7 +51,7 @@ export default defineConfig({
51
51
 
52
52
  // ⚔ Dev server settings
53
53
  server: {
54
- port: 5173,
54
+ port: 2202,
55
55
  },
56
56
 
57
57
  // šŸ—ļø Build options
@@ -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 || 5173;
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.20",
3
+ "version": "1.0.22",
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",