react-client 1.0.38 → 1.0.41

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.
Files changed (47) hide show
  1. package/README.md +115 -124
  2. package/dist/cli/commands/build.js +19 -3
  3. package/dist/cli/commands/build.js.map +1 -1
  4. package/dist/cli/commands/dev.js +373 -102
  5. package/dist/cli/commands/dev.js.map +1 -1
  6. package/dist/cli/commands/init.js +72 -7
  7. package/dist/cli/commands/init.js.map +1 -1
  8. package/dist/cli/commands/preview.js +9 -15
  9. package/dist/cli/commands/preview.js.map +1 -1
  10. package/dist/cli/index.js +1 -0
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +3 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/utils/loadConfig.js +31 -22
  16. package/dist/utils/loadConfig.js.map +1 -1
  17. package/package.json +3 -2
  18. package/templates/react/public/favicon.ico +0 -0
  19. package/templates/react/public/index.html +14 -0
  20. package/templates/react/public/logo512.png +0 -0
  21. package/templates/react/src/App.css +42 -0
  22. package/templates/react/src/App.jsx +23 -1
  23. package/templates/react/src/index.css +68 -0
  24. package/templates/react-tailwind/public/favicon.ico +0 -0
  25. package/templates/react-tailwind/public/index.html +14 -0
  26. package/templates/react-tailwind/public/logo512.png +0 -0
  27. package/templates/react-tailwind/src/App.css +42 -0
  28. package/templates/react-tailwind/src/App.jsx +31 -2
  29. package/templates/react-tailwind/src/index.css +68 -1
  30. package/templates/react-tailwind/src/main.jsx +1 -3
  31. package/templates/react-tailwind-ts/public/favicon.ico +0 -0
  32. package/templates/react-tailwind-ts/public/index.html +14 -0
  33. package/templates/react-tailwind-ts/public/logo512.png +0 -0
  34. package/templates/react-tailwind-ts/src/App.css +42 -0
  35. package/templates/react-tailwind-ts/src/App.tsx +30 -2
  36. package/templates/react-tailwind-ts/src/index.css +68 -1
  37. package/templates/react-tailwind-ts/src/main.tsx +0 -1
  38. package/templates/react-ts/public/favicon.ico +0 -0
  39. package/templates/react-ts/public/index.html +14 -0
  40. package/templates/react-ts/public/logo512.png +0 -0
  41. package/templates/react-ts/src/App.css +42 -0
  42. package/templates/react-ts/src/App.tsx +23 -1
  43. package/templates/react-ts/src/index.css +68 -0
  44. package/templates/react/index.html +0 -13
  45. package/templates/react-tailwind/index.html +0 -13
  46. package/templates/react-tailwind-ts/index.html +0 -13
  47. package/templates/react-ts/index.html +0 -13
@@ -14,7 +14,6 @@ import connect from 'connect';
14
14
  import http from 'http';
15
15
  import chokidar from 'chokidar';
16
16
  import detectPort from 'detect-port';
17
- import prompts from 'prompts';
18
17
  import path from 'path';
19
18
  import fs from 'fs-extra';
20
19
  import open from 'open';
@@ -23,13 +22,11 @@ import { execSync } from 'child_process';
23
22
  import { BroadcastManager } from '../../server/broadcastManager.js';
24
23
  import { createRequire } from 'module';
25
24
  import { fileURLToPath } from 'url';
26
- import { dirname, resolve } from 'path';
25
+ import { dirname } from 'path';
26
+ import { loadReactClientConfig } from '../../utils/loadConfig.js';
27
27
  const __filename = fileURLToPath(import.meta.url);
28
28
  const __dirname = dirname(__filename);
29
- const loadConfigPath = resolve(__dirname, '../../utils/loadConfig.js');
30
- const { loadReactClientConfig } = await import(loadConfigPath);
31
29
  const require = createRequire(import.meta.url);
32
- const RUNTIME_OVERLAY_ROUTE = '/@runtime/overlay';
33
30
  function jsContentType() {
34
31
  return 'application/javascript; charset=utf-8';
35
32
  }
@@ -178,32 +175,37 @@ export default async function dev() {
178
175
  const root = process.cwd();
179
176
  const userConfig = (await loadReactClientConfig(root));
180
177
  const appRoot = path.resolve(root, userConfig.root || '.');
181
- const defaultPort = userConfig.server?.port ?? 2202;
178
+ const defaultPort = Number(process.env.PORT) || userConfig.server?.port || 2202;
182
179
  // cache dir for prebundled deps
183
180
  const cacheDir = path.join(appRoot, '.react-client', 'deps');
184
181
  await fs.ensureDir(cacheDir);
185
182
  // Detect entry (main.tsx / main.jsx)
186
- const possible = ['src/main.tsx', 'src/main.jsx'].map((p) => path.join(appRoot, p));
187
- const entry = possible.find((p) => fs.existsSync(p));
183
+ const paths = [
184
+ path.join(appRoot, 'src/main.tsx'),
185
+ path.join(appRoot, 'src/main.jsx'),
186
+ path.join(appRoot, 'main.tsx'),
187
+ path.join(appRoot, 'main.jsx'),
188
+ ];
189
+ const entry = paths.find((p) => fs.existsSync(p));
188
190
  if (!entry) {
189
- console.error(chalk.red('❌ Entry not found: src/main.tsx or src/main.jsx'));
191
+ console.error(chalk.red('❌ Entry not found: main.tsx or main.jsx in app root or src/'));
190
192
  process.exit(1);
191
193
  }
192
- const indexHtml = path.join(appRoot, 'index.html');
194
+ // Detect index.html and public dir
195
+ let publicDir = path.join(appRoot, 'public');
196
+ if (!fs.existsSync(publicDir)) {
197
+ publicDir = path.join(root, 'public');
198
+ if (!fs.existsSync(publicDir)) {
199
+ // Create empty if missing, but usually templates provide it
200
+ await fs.ensureDir(publicDir);
201
+ }
202
+ }
203
+ const indexHtml = path.join(publicDir, 'index.html');
193
204
  // Select port
194
205
  const availablePort = await detectPort(defaultPort);
195
206
  const port = availablePort;
196
207
  if (availablePort !== defaultPort) {
197
- const response = await prompts({
198
- type: 'confirm',
199
- name: 'useNewPort',
200
- message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
201
- initial: true,
202
- });
203
- if (!response.useNewPort) {
204
- console.log('🛑 Dev server cancelled.');
205
- process.exit(0);
206
- }
208
+ console.log(chalk.yellow(`\n⚠️ Port ${defaultPort} is occupied. Using ${availablePort} instead.`));
207
209
  }
208
210
  // Ensure react-refresh runtime available (used by many templates)
209
211
  try {
@@ -239,6 +241,37 @@ export default async function dev() {
239
241
  return code;
240
242
  },
241
243
  },
244
+ {
245
+ name: 'react-refresh',
246
+ async onTransform(code, id) {
247
+ if (id.match(/\.[tj]sx$/)) {
248
+ // In ESM, we can't easily put statements before imports.
249
+ // We'll rely on the global hook injected in index.html.
250
+ const relativePath = '/' + path.relative(appRoot, id);
251
+ const hmrBoilerplate = `
252
+ if (window.__REFRESH_RUNTIME__ && window.__GET_HOT_CONTEXT__) {
253
+ const ___hot = window.__GET_HOT_CONTEXT__(${JSON.stringify(relativePath)});
254
+ if (___hot) {
255
+ window.$RefreshReg$ = (type, id) => {
256
+ window.__REFRESH_RUNTIME__.register(type, ${JSON.stringify(relativePath)} + " " + id);
257
+ };
258
+ window.$RefreshSig$ = () => window.__REFRESH_RUNTIME__.createSignatureFunctionForTransform();
259
+ }
260
+ }
261
+ `;
262
+ const modBoilerplate = `
263
+ if (window.__RC_HMR_STATE__) {
264
+ const ___mod = window.__RC_HMR_STATE__.modules[${JSON.stringify(relativePath)}];
265
+ if (___mod && ___mod.cb) {
266
+ if (typeof ___mod.cb === 'function') ___mod.cb();
267
+ }
268
+ }
269
+ `;
270
+ return `${code}\n${hmrBoilerplate}\n${modBoilerplate}`;
271
+ }
272
+ return code;
273
+ },
274
+ },
242
275
  ];
243
276
  const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
244
277
  const plugins = [...corePlugins, ...userPlugins];
@@ -246,67 +279,192 @@ export default async function dev() {
246
279
  const app = connect();
247
280
  const transformCache = new Map();
248
281
  // Helper: recursively analyze dependency graph for prebundling (bare imports)
249
- async function analyzeGraph(file, seen = new Set()) {
250
- if (seen.has(file))
251
- return seen;
252
- seen.add(file);
253
- try {
254
- const code = await fs.readFile(file, 'utf8');
255
- const matches = [
256
- ...code.matchAll(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g),
257
- ...code.matchAll(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g),
258
- ];
259
- for (const m of matches) {
260
- const dep = m[1];
261
- if (!dep || dep.startsWith('.') || dep.startsWith('/'))
262
- continue;
263
- try {
264
- const resolved = require.resolve(dep, { paths: [appRoot] });
265
- await analyzeGraph(resolved, seen);
266
- }
267
- catch {
268
- // bare dependency (node_modules) - track name
269
- seen.add(dep);
282
+ // --- Dependency Analysis & Prebundling ---
283
+ async function analyzeGraph(file, _seen = new Set()) {
284
+ const deps = new Set();
285
+ const visitedFiles = new Set();
286
+ async function walk(f) {
287
+ if (visitedFiles.has(f))
288
+ return;
289
+ visitedFiles.add(f);
290
+ try {
291
+ const code = await fs.readFile(f, 'utf8');
292
+ const matches = [
293
+ ...code.matchAll(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g),
294
+ ...code.matchAll(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g),
295
+ ...code.matchAll(/\brequire\(['"]([^'".\/][^'"]*)['"]\)/g),
296
+ ];
297
+ for (const m of matches) {
298
+ const dep = m[1];
299
+ if (!dep || dep.startsWith('.') || dep.startsWith('/'))
300
+ continue;
301
+ if (!deps.has(dep)) {
302
+ deps.add(dep);
303
+ try {
304
+ const resolved = require.resolve(dep, { paths: [appRoot] });
305
+ if (resolved.includes('node_modules')) {
306
+ await walk(resolved);
307
+ }
308
+ }
309
+ catch {
310
+ // skip unresolvable
311
+ }
312
+ }
270
313
  }
271
314
  }
315
+ catch {
316
+ // skip missing files
317
+ }
272
318
  }
273
- catch {
274
- // ignore unreadable files
275
- }
276
- return seen;
319
+ await walk(file);
320
+ return deps;
277
321
  }
278
- // Prebundle dependencies into cache dir (parallel)
322
+ // Helper: esbuild plugin to rewrite bare imports in dependency bundles to /@modules/
323
+ const dependencyBundlePlugin = {
324
+ name: 'dependency-bundle-plugin',
325
+ setup(build) {
326
+ // Intercept any bare import (not starting with . or /) that is NOT the entry point
327
+ build.onResolve({ filter: /^[^.\/]/ }, (args) => {
328
+ // If this is the initial entry point, don't externalize it
329
+ if (args.kind === 'entry-point')
330
+ return null;
331
+ // Otherwise, externalize and point to /@modules/
332
+ return {
333
+ path: `/@modules/${args.path}`,
334
+ external: true,
335
+ };
336
+ });
337
+ },
338
+ };
339
+ // Prebundle dependencies into cache dir using code-splitting
279
340
  async function prebundleDeps(deps) {
280
341
  if (!deps.size)
281
342
  return;
282
- const existingFiles = await fs.readdir(cacheDir);
283
- const existing = new Set(existingFiles.map((f) => f.replace(/\.js$/, '')));
284
- const missing = [...deps].filter((d) => !existing.has(d));
285
- if (!missing.length)
286
- return;
287
- console.log(chalk.cyan('📦 Prebundling:'), missing.join(', '));
288
- await Promise.all(missing.map(async (dep) => {
343
+ const entryPoints = {};
344
+ const depsArray = [...deps];
345
+ // Create a temp directory for proxy files
346
+ const proxyDir = path.join(appRoot, '.react-client', 'proxies');
347
+ await fs.ensureDir(proxyDir);
348
+ for (const dep of depsArray) {
289
349
  try {
290
- const entryPoint = require.resolve(dep, { paths: [appRoot] });
291
- const outFile = path.join(cacheDir, normalizeCacheKey(dep) + '.js');
292
- await esbuild.build({
293
- entryPoints: [entryPoint],
294
- bundle: true,
295
- platform: 'browser',
296
- format: 'esm',
297
- outfile: outFile,
298
- write: true,
299
- target: ['es2020'],
300
- });
301
- console.log(chalk.green(`✅ Cached ${dep}`));
350
+ const resolved = require.resolve(dep, { paths: [appRoot] });
351
+ const key = normalizeCacheKey(dep);
352
+ const proxyPath = path.join(proxyDir, `${key}.js`);
353
+ const resolvedPath = JSON.stringify(resolved);
354
+ let proxyCode = '';
355
+ // Precision Proxy: hardcoded exports for most critical React dependencies
356
+ const reactKeys = [
357
+ 'useState',
358
+ 'useEffect',
359
+ 'useContext',
360
+ 'useReducer',
361
+ 'useCallback',
362
+ 'useMemo',
363
+ 'useRef',
364
+ 'useImperativeHandle',
365
+ 'useLayoutEffect',
366
+ 'useDebugValue',
367
+ 'useDeferredValue',
368
+ 'useTransition',
369
+ 'useId',
370
+ 'useInsertionEffect',
371
+ 'useSyncExternalStore',
372
+ 'createElement',
373
+ 'createContext',
374
+ 'createRef',
375
+ 'forwardRef',
376
+ 'memo',
377
+ 'lazy',
378
+ 'Suspense',
379
+ 'Fragment',
380
+ 'Profiler',
381
+ 'StrictMode',
382
+ 'Children',
383
+ 'Component',
384
+ 'PureComponent',
385
+ 'cloneElement',
386
+ 'isValidElement',
387
+ 'createFactory',
388
+ 'version',
389
+ 'startTransition',
390
+ ];
391
+ const reactDomClientKeys = ['createRoot', 'hydrateRoot'];
392
+ const reactDomKeys = [
393
+ 'render',
394
+ 'hydrate',
395
+ 'unmountComponentAtNode',
396
+ 'findDOMNode',
397
+ 'createPortal',
398
+ 'version',
399
+ 'flushSync',
400
+ ];
401
+ const jsxRuntimeKeys = ['jsx', 'jsxs', 'Fragment'];
402
+ if (dep === 'react') {
403
+ proxyCode = `import * as m from ${resolvedPath}; export const { ${reactKeys.join(', ')} } = m; export default (m.default || m);`;
404
+ }
405
+ else if (dep === 'react-dom/client') {
406
+ proxyCode = `import * as m from ${resolvedPath}; export const { ${reactDomClientKeys.join(', ')} } = m; export default (m.default || m);`;
407
+ }
408
+ else if (dep === 'react-dom') {
409
+ proxyCode = `import * as m from ${resolvedPath}; export const { ${reactDomKeys.join(', ')} } = m; export default (m.default || m);`;
410
+ }
411
+ else if (dep === 'react/jsx-runtime' || dep === 'react/jsx-dev-runtime') {
412
+ proxyCode = `import * as m from ${resolvedPath}; export const { ${jsxRuntimeKeys.join(', ')} } = m; export default (m.default || m);`;
413
+ }
414
+ else {
415
+ try {
416
+ // Dynamic Proxy Generation for other deps
417
+ const m = require(resolved);
418
+ const keys = Object.keys(m).filter((k) => k !== 'default' && k !== '__esModule');
419
+ if (keys.length > 0) {
420
+ proxyCode = `import * as m from ${resolvedPath}; export const { ${keys.join(', ')} } = m; export default (m.default || m);`;
421
+ }
422
+ else {
423
+ proxyCode = `import _default from ${resolvedPath}; export default _default;`;
424
+ }
425
+ }
426
+ catch {
427
+ proxyCode = `export * from ${resolvedPath}; import _default from ${resolvedPath}; export default _default;`;
428
+ }
429
+ }
430
+ await fs.writeFile(proxyPath, proxyCode, 'utf8');
431
+ entryPoints[key] = proxyPath;
302
432
  }
303
433
  catch (err) {
304
- console.warn(chalk.yellow(`⚠️ Skipped ${dep}: ${err.message}`));
434
+ console.warn(chalk.yellow(`⚠️ Could not resolve ${dep}: ${err.message}`));
305
435
  }
306
- }));
436
+ }
437
+ if (Object.keys(entryPoints).length === 0)
438
+ return;
439
+ console.log(chalk.cyan('📦 Prebundling dependencies with precision proxies...'));
440
+ try {
441
+ await esbuild.build({
442
+ entryPoints,
443
+ bundle: true,
444
+ splitting: true, // Re-enable splitting for shared dependency chunks
445
+ format: 'esm',
446
+ outdir: cacheDir,
447
+ platform: 'browser',
448
+ target: ['es2020'],
449
+ minify: false,
450
+ plugins: [], // NO external plugins during prebundle, let esbuild manage the graph
451
+ define: {
452
+ 'process.env.NODE_ENV': '"development"',
453
+ },
454
+ logLevel: 'error',
455
+ });
456
+ // Cleanup proxy dir after build
457
+ await fs.remove(proxyDir).catch(() => { });
458
+ console.log(chalk.green('✅ Prebundling complete.'));
459
+ }
460
+ catch (err) {
461
+ console.error(chalk.red(`❌ Prebundling failed: ${err.message}`));
462
+ }
307
463
  }
308
464
  // Build initial prebundle graph from entry
309
465
  const depsSet = await analyzeGraph(entry);
466
+ // Ensure react/jsx-runtime is prebundled if used
467
+ depsSet.add('react/jsx-runtime');
310
468
  await prebundleDeps(depsSet);
311
469
  // Watch package.json for changes to re-prebundle
312
470
  const pkgPath = path.join(appRoot, 'package.json');
@@ -314,12 +472,45 @@ export default async function dev() {
314
472
  chokidar.watch(pkgPath).on('change', async () => {
315
473
  console.log(chalk.yellow('📦 package.json changed — rebuilding prebundle...'));
316
474
  const newDeps = await analyzeGraph(entry);
475
+ newDeps.add('react/jsx-runtime');
317
476
  await prebundleDeps(newDeps);
318
477
  });
319
478
  }
320
479
  // --- Serve /@modules/<dep> (prebundled or on-demand esbuild bundle)
321
480
  app.use((async (req, res, next) => {
322
481
  const url = req.url ?? '';
482
+ // Serve React Refresh runtime
483
+ if (url === '/@react-refresh') {
484
+ res.setHeader('Content-Type', jsContentType());
485
+ try {
486
+ const runtimePath = require.resolve('react-refresh/runtime');
487
+ // Bundle it to ESM for the browser
488
+ const bundled = await esbuild.build({
489
+ entryPoints: [runtimePath],
490
+ bundle: true,
491
+ format: 'iife',
492
+ globalName: '__REFRESH_RUNTIME__',
493
+ write: false,
494
+ minify: true,
495
+ define: {
496
+ 'process.env.NODE_ENV': '"development"',
497
+ },
498
+ });
499
+ const runtimeCode = bundled.outputFiles?.[0]?.text ?? '';
500
+ return res.end(`
501
+ const prevRefreshReg = window.$RefreshReg$;
502
+ const prevRefreshSig = window.$RefreshSig$;
503
+ ${runtimeCode}
504
+ window.$RefreshReg$ = prevRefreshReg;
505
+ window.$RefreshSig$ = prevRefreshSig;
506
+ export default window.__REFRESH_RUNTIME__;
507
+ `);
508
+ }
509
+ catch (err) {
510
+ res.writeHead(500);
511
+ return res.end(`// react-refresh runtime error: ${err.message}`);
512
+ }
513
+ }
323
514
  if (!url.startsWith('/@modules/'))
324
515
  return next();
325
516
  const id = url.replace(/^\/@modules\//, '');
@@ -328,12 +519,25 @@ export default async function dev() {
328
519
  return res.end('// invalid module');
329
520
  }
330
521
  try {
331
- const cacheFile = path.join(cacheDir, normalizeCacheKey(id) + '.js');
522
+ // 1. Check if it's a file in the cache directory (prebundled or shared chunk)
523
+ // Chunks might be requested via /@modules/dep/chunk-xxx.js or just /@modules/chunk-xxx.js
524
+ const idBase = path.basename(id);
525
+ const cacheFile = id.endsWith('.js')
526
+ ? path.join(cacheDir, id)
527
+ : path.join(cacheDir, normalizeCacheKey(id) + '.js');
528
+ const cacheFileAlternative = path.join(cacheDir, idBase);
529
+ let foundCacheFile = '';
332
530
  if (await fs.pathExists(cacheFile)) {
531
+ foundCacheFile = cacheFile;
532
+ }
533
+ else if (await fs.pathExists(cacheFileAlternative)) {
534
+ foundCacheFile = cacheFileAlternative;
535
+ }
536
+ if (foundCacheFile) {
333
537
  res.setHeader('Content-Type', jsContentType());
334
- return res.end(await fs.readFile(cacheFile, 'utf8'));
538
+ return res.end(await fs.readFile(foundCacheFile, 'utf8'));
335
539
  }
336
- // Resolve the actual entry file (handles subpaths & package exports)
540
+ // 2. Resolve the actual entry file for bare imports
337
541
  const entryFile = await resolveModuleEntry(id, appRoot);
338
542
  const result = await esbuild.build({
339
543
  entryPoints: [entryFile],
@@ -342,6 +546,12 @@ export default async function dev() {
342
546
  format: 'esm',
343
547
  write: false,
344
548
  target: ['es2020'],
549
+ jsx: 'automatic',
550
+ // Critical: use dependencyBundlePlugin to ensure sub-deps are rewritten to /@modules/
551
+ plugins: [dependencyBundlePlugin],
552
+ define: {
553
+ 'process.env.NODE_ENV': '"development"',
554
+ },
345
555
  });
346
556
  const output = result.outputFiles?.[0]?.text ?? '';
347
557
  // Write cache and respond
@@ -356,10 +566,6 @@ export default async function dev() {
356
566
  }));
357
567
  // --- Serve runtime overlay (inline, no external dependencies)
358
568
  const OVERLAY_RUNTIME = `
359
- /* inline overlay runtime - served at ${RUNTIME_OVERLAY_ROUTE} */
360
- ${(() => {
361
- // small helper — embed as a string
362
- return `
363
569
  const overlayId = "__rc_error_overlay__";
364
570
  (function(){
365
571
  const style = document.createElement("style");
@@ -416,15 +622,13 @@ const overlayId = "__rc_error_overlay__";
416
622
  window.addEventListener("unhandledrejection", e => window.showErrorOverlay?.(e.reason || e));
417
623
  })();
418
624
  `;
419
- })()}
420
- `;
421
- app.use(async (req, res, next) => {
422
- if (req.url === RUNTIME_OVERLAY_ROUTE) {
625
+ app.use((async (req, res, next) => {
626
+ if (req.url === '/@runtime/overlay') {
423
627
  res.setHeader('Content-Type', jsContentType());
424
628
  return res.end(OVERLAY_RUNTIME);
425
629
  }
426
630
  next();
427
- });
631
+ }));
428
632
  // --- minimal /@source-map: return snippet around requested line of original source file
429
633
  app.use((async (req, res, next) => {
430
634
  const url = req.url ?? '';
@@ -458,13 +662,37 @@ const overlayId = "__rc_error_overlay__";
458
662
  })
459
663
  .join('\n');
460
664
  res.setHeader('Content-Type', 'application/json');
461
- res.end(JSON.stringify({ source: file, line: lineNum, column: 0, snippet }));
665
+ res.end(JSON.stringify({ source: filePath, line: lineNum, column: 0, snippet }));
462
666
  }
463
667
  catch (err) {
464
668
  res.writeHead(500);
465
669
  res.end(JSON.stringify({ error: err.message }));
466
670
  }
467
671
  }));
672
+ // --- Serve public/ files as static assets
673
+ app.use((async (req, res, next) => {
674
+ const raw = decodeURIComponent((req.url ?? '').split('?')[0]);
675
+ const publicFile = path.join(publicDir, raw.replace(/^\//, ''));
676
+ if ((await fs.pathExists(publicFile)) && !(await fs.stat(publicFile)).isDirectory()) {
677
+ const ext = path.extname(publicFile).toLowerCase();
678
+ // Simple content type map
679
+ const types = {
680
+ '.html': 'text/html',
681
+ '.js': 'application/javascript',
682
+ '.css': 'text/css',
683
+ '.json': 'application/json',
684
+ '.png': 'image/png',
685
+ '.jpg': 'image/jpeg',
686
+ '.svg': 'image/svg+xml',
687
+ '.ico': 'image/x-icon',
688
+ };
689
+ const content = await fs.readFile(publicFile);
690
+ res.setHeader('Content-Type', types[ext] || 'application/octet-stream');
691
+ res.setHeader('Content-Length', content.length);
692
+ return res.end(content);
693
+ }
694
+ next();
695
+ }));
468
696
  // --- Serve /src/* files (on-the-fly transform + bare import rewrite)
469
697
  app.use((async (req, res, next) => {
470
698
  const url = req.url ?? '';
@@ -485,10 +713,6 @@ const overlayId = "__rc_error_overlay__";
485
713
  return next();
486
714
  try {
487
715
  let code = await fs.readFile(found, 'utf8');
488
- // rewrite bare imports -> /@modules/<dep>
489
- code = code
490
- .replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
491
- .replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
492
716
  // run plugin transforms
493
717
  for (const p of plugins) {
494
718
  if (p.onTransform) {
@@ -497,17 +721,28 @@ const overlayId = "__rc_error_overlay__";
497
721
  code = out;
498
722
  }
499
723
  }
500
- // choose loader by extension
501
724
  const ext = path.extname(found).toLowerCase();
502
725
  const loader = ext === '.ts' ? 'ts' : ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'js';
503
726
  const result = await esbuild.transform(code, {
504
727
  loader,
505
728
  sourcemap: 'inline',
506
729
  target: ['es2020'],
730
+ jsx: 'automatic',
507
731
  });
508
- transformCache.set(found, result.code);
732
+ let transformedCode = result.code;
733
+ // Inject HMR/Refresh boilerplate (ESM-Safe: use global accessors and append logic)
734
+ const modulePath = '/' + path.relative(appRoot, found).replace(/\\/g, '/');
735
+ // 1. Replace import.meta.hot with a global context accessor (safe anywhere in ESM)
736
+ transformedCode = transformedCode.replace(/import\.meta\.hot/g, `window.__GET_HOT_CONTEXT__?.(${JSON.stringify(modulePath)})`);
737
+ // rewrite bare imports -> /@modules/<dep>
738
+ transformedCode = transformedCode
739
+ .replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
740
+ .replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`)
741
+ .replace(/^(import\s+['"])([^'".\/][^'"]*)(['"])/gm, (_m, start, dep, end) => `${start}/@modules/${dep}${end}`)
742
+ .replace(/^(export\s+\*\s+from\s+['"])([^'".\/][^'"]*)(['"])/gm, (_m, start, dep, end) => `${start}/@modules/${dep}${end}`);
743
+ transformCache.set(found, transformedCode);
509
744
  res.setHeader('Content-Type', jsContentType());
510
- res.end(result.code);
745
+ res.end(transformedCode);
511
746
  }
512
747
  catch (err) {
513
748
  const e = err;
@@ -525,10 +760,25 @@ const overlayId = "__rc_error_overlay__";
525
760
  return res.end('index.html not found');
526
761
  }
527
762
  try {
528
- let html = await fs.readFile(indexHtml, 'utf8');
529
- // inject overlay runtime and HMR client if not already present
530
- if (!html.includes(RUNTIME_OVERLAY_ROUTE)) {
531
- html = html.replace('</body>', `\n<script type="module" src="${RUNTIME_OVERLAY_ROUTE}"></script>\n<script type="module">
763
+ const html = await fs.readFile(indexHtml, 'utf8');
764
+ // React Refresh Preamble for index.html
765
+ const reactRefreshPreamble = `
766
+ <script type="module">
767
+ import RefreshRuntime from "/@react-refresh";
768
+ RefreshRuntime.injectIntoGlobalHook(window);
769
+ window.$RefreshReg$ = () => {};
770
+ window.$RefreshSig$ = () => (type) => type;
771
+ window.__REFRESH_RUNTIME__ = RefreshRuntime;
772
+ </script>
773
+ <script type="module" src="/@runtime/overlay"></script>
774
+ <script type="module">
775
+ window.__RC_HMR_STATE__ = { modules: {} };
776
+ window.__GET_HOT_CONTEXT__ = (id) => {
777
+ return window.__RC_HMR_STATE__.modules[id] || (window.__RC_HMR_STATE__.modules[id] = {
778
+ id,
779
+ accept: (cb) => { window.__RC_HMR_STATE__.modules[id].cb = cb || true; }
780
+ });
781
+ };
532
782
  const ws = new WebSocket("ws://" + location.host);
533
783
  ws.onmessage = (e) => {
534
784
  const msg = JSON.parse(e.data);
@@ -536,13 +786,25 @@ const overlayId = "__rc_error_overlay__";
536
786
  if (msg.type === "error") window.showErrorOverlay?.(msg);
537
787
  if (msg.type === "update") {
538
788
  window.clearErrorOverlay?.();
539
- import(msg.path + "?t=" + Date.now());
789
+ const mod = window.__RC_HMR_STATE__.modules[msg.path];
790
+ if (mod && mod.cb) {
791
+ import(msg.path + "?t=" + Date.now()).then(() => {
792
+ if (typeof mod.cb === 'function') mod.cb();
793
+ // Trigger Fast Refresh after module update
794
+ if (window.__REFRESH_RUNTIME__) {
795
+ window.__REFRESH_RUNTIME__.performReactRefresh();
796
+ }
797
+ });
798
+ } else {
799
+ location.reload();
800
+ }
540
801
  }
541
802
  };
542
- </script>\n</body>`);
543
- }
803
+ </script>`.trim();
804
+ // Inject preamble at the top of <body>
805
+ const newHtml = html.replace('<body>', `<body>\n${reactRefreshPreamble}`);
544
806
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
545
- res.end(html);
807
+ res.end(newHtml);
546
808
  }
547
809
  catch (err) {
548
810
  res.writeHead(500);
@@ -593,12 +855,21 @@ const overlayId = "__rc_error_overlay__";
593
855
  }
594
856
  });
595
857
  // graceful shutdown
596
- process.on('SIGINT', async () => {
597
- console.log(chalk.red('\n🛑 Shutting down...'));
598
- await watcher.close();
599
- broadcaster.close();
600
- server.close();
601
- process.exit(0);
602
- });
858
+ const shutdown = async () => {
859
+ console.log(chalk.red('\n🛑 Shutting down dev server...'));
860
+ try {
861
+ await watcher.close();
862
+ broadcaster.close();
863
+ server.close();
864
+ }
865
+ catch (err) {
866
+ console.error(chalk.red('⚠️ Error during shutdown:'), err.message);
867
+ }
868
+ finally {
869
+ process.exit(0);
870
+ }
871
+ };
872
+ process.on('SIGINT', shutdown);
873
+ process.on('SIGTERM', shutdown);
603
874
  }
604
875
  //# sourceMappingURL=dev.js.map