react-client 1.0.37 → 1.0.40

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 (51) 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.d.ts +5 -6
  5. package/dist/cli/commands/dev.js +505 -231
  6. package/dist/cli/commands/dev.js.map +1 -1
  7. package/dist/cli/commands/init.js +67 -7
  8. package/dist/cli/commands/init.js.map +1 -1
  9. package/dist/cli/commands/preview.js +9 -15
  10. package/dist/cli/commands/preview.js.map +1 -1
  11. package/dist/cli/index.js +1 -0
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.js +3 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/server/broadcastManager.d.ts +18 -20
  17. package/dist/server/broadcastManager.js +13 -33
  18. package/dist/server/broadcastManager.js.map +1 -1
  19. package/dist/utils/loadConfig.js +31 -22
  20. package/dist/utils/loadConfig.js.map +1 -1
  21. package/package.json +3 -2
  22. package/templates/react/public/favicon.ico +0 -0
  23. package/templates/react/public/index.html +14 -0
  24. package/templates/react/public/logo512.png +0 -0
  25. package/templates/react/src/App.css +42 -0
  26. package/templates/react/src/App.jsx +23 -1
  27. package/templates/react/src/index.css +68 -0
  28. package/templates/react-tailwind/public/favicon.ico +0 -0
  29. package/templates/react-tailwind/public/index.html +14 -0
  30. package/templates/react-tailwind/public/logo512.png +0 -0
  31. package/templates/react-tailwind/src/App.css +42 -0
  32. package/templates/react-tailwind/src/App.jsx +31 -2
  33. package/templates/react-tailwind/src/index.css +68 -1
  34. package/templates/react-tailwind/src/main.jsx +1 -3
  35. package/templates/react-tailwind-ts/public/favicon.ico +0 -0
  36. package/templates/react-tailwind-ts/public/index.html +14 -0
  37. package/templates/react-tailwind-ts/public/logo512.png +0 -0
  38. package/templates/react-tailwind-ts/src/App.css +42 -0
  39. package/templates/react-tailwind-ts/src/App.tsx +30 -2
  40. package/templates/react-tailwind-ts/src/index.css +68 -1
  41. package/templates/react-tailwind-ts/src/main.tsx +0 -1
  42. package/templates/react-ts/public/favicon.ico +0 -0
  43. package/templates/react-ts/public/index.html +14 -0
  44. package/templates/react-ts/public/logo512.png +0 -0
  45. package/templates/react-ts/src/App.css +42 -0
  46. package/templates/react-ts/src/App.tsx +23 -1
  47. package/templates/react-ts/src/index.css +68 -0
  48. package/templates/react/index.html +0 -13
  49. package/templates/react-tailwind/index.html +0 -13
  50. package/templates/react-tailwind-ts/index.html +0 -13
  51. package/templates/react-ts/index.html +0 -13
@@ -1,48 +1,51 @@
1
- // src/cli/commands/dev.ts
2
1
  /**
3
- * dev.ts — Vite-like dev server for react-client (updated)
2
+ * dev.ts — dev server for react-client
4
3
  *
5
- * - resolves package export fields & subpaths (resolveModuleEntry)
6
4
  * - prebundles deps into .react-client/deps
7
- * - serves /@modules/<dep> (prebundled or on-demand esbuild bundle)
8
- * - serves /src/* with esbuild transform + inline sourcemap
9
- * - serves local overlay runtime at /@runtime/overlay if src/runtime/overlay-runtime.js exists
5
+ * - serves /@modules/<dep>
6
+ * - serves /src/* with esbuild transform & inline sourcemap
10
7
  * - /@source-map returns a snippet for overlay mapping
11
8
  * - HMR broadcast via BroadcastManager (ws)
12
- * - plugin system: onTransform, onHotUpdate, onServe, onServerStart
9
+ *
10
+ * Keep this file linted & typed. Avoids manual react-dom/client hacks.
13
11
  */
14
12
  import esbuild from 'esbuild';
15
13
  import connect from 'connect';
16
14
  import http from 'http';
17
15
  import chokidar from 'chokidar';
18
16
  import detectPort from 'detect-port';
19
- import prompts from 'prompts';
20
17
  import path from 'path';
21
18
  import fs from 'fs-extra';
22
19
  import open from 'open';
23
20
  import chalk from 'chalk';
24
21
  import { execSync } from 'child_process';
25
- import { loadReactClientConfig } from '../../utils/loadConfig.js';
26
22
  import { BroadcastManager } from '../../server/broadcastManager.js';
27
- const RUNTIME_OVERLAY_ROUTE = '/@runtime/overlay';
23
+ import { createRequire } from 'module';
24
+ import { fileURLToPath } from 'url';
25
+ import { dirname } from 'path';
26
+ import { loadReactClientConfig } from '../../utils/loadConfig.js';
27
+ const __filename = fileURLToPath(import.meta.url);
28
+ const __dirname = dirname(__filename);
29
+ const require = createRequire(import.meta.url);
28
30
  function jsContentType() {
29
31
  return 'application/javascript; charset=utf-8';
30
32
  }
31
33
  /**
32
- * Resolve a package entry robustly:
33
- * - try require.resolve(id)
34
- * - try package.json exports field + subpath resolution
35
- * - fallback to common fields (module/main/browser)
34
+ * Resolve any bare import id robustly:
35
+ * 1. try require.resolve(id)
36
+ * 2. try require.resolve(`${pkg}/${subpath}`)
37
+ * 3. try package.json exports field
38
+ * 4. try common fallback candidates
36
39
  */
37
40
  async function resolveModuleEntry(id, root) {
38
- // quick path
41
+ // quick resolution
39
42
  try {
40
43
  return require.resolve(id, { paths: [root] });
41
44
  }
42
45
  catch {
43
46
  // continue
44
47
  }
45
- // split package root and possible subpath
48
+ // split package root and subpath
46
49
  const parts = id.split('/');
47
50
  const pkgRoot = parts[0].startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
48
51
  const subPath = parts.slice(pkgRoot.startsWith('@') ? 2 : 1).join('/');
@@ -51,100 +54,120 @@ async function resolveModuleEntry(id, root) {
51
54
  pkgJsonPath = require.resolve(`${pkgRoot}/package.json`, { paths: [root] });
52
55
  }
53
56
  catch {
57
+ // No need to keep unused variable 'err'
54
58
  throw new Error(`Package not found: ${pkgRoot}`);
55
59
  }
56
60
  const pkgDir = path.dirname(pkgJsonPath);
61
+ // Explicitly type pkgJson to avoid 'any'
57
62
  let pkgJson = {};
58
63
  try {
59
- const raw = await fs.readFile(pkgJsonPath, 'utf8');
60
- pkgJson = JSON.parse(raw);
64
+ const pkgContent = await fs.readFile(pkgJsonPath, 'utf8');
65
+ pkgJson = JSON.parse(pkgContent);
61
66
  }
62
67
  catch {
63
- /* ignore */
68
+ // ignore parse or read errors gracefully
64
69
  }
65
- // handle "exports"
70
+ // If exports field exists, try to look up subpath (type-safe, supports conditional exports)
66
71
  if (pkgJson.exports) {
67
72
  const exportsField = pkgJson.exports;
73
+ // If exports is a plain string -> it's the entry
68
74
  if (typeof exportsField === 'string') {
69
- if (!subPath) {
70
- const candidate = path.resolve(pkgDir, exportsField);
71
- if (await fs.pathExists(candidate))
72
- return candidate;
73
- }
75
+ if (!subPath)
76
+ return path.resolve(pkgDir, exportsField);
74
77
  }
75
78
  else if (exportsField && typeof exportsField === 'object') {
79
+ // Normalize to a record so we can index it safely
76
80
  const exportsMap = exportsField;
77
- const candidates = [];
81
+ // Try candidates in order: explicit subpath, index, fallback
82
+ const keyCandidates = [];
78
83
  if (subPath) {
79
- candidates.push(`./${subPath}`, `./${subPath}.js`, `./${subPath}.mjs`);
84
+ keyCandidates.push(`./${subPath}`, `./${subPath}.js`, `./${subPath}.mjs`);
80
85
  }
81
- candidates.push('.', './index.js', './index.mjs');
82
- for (const key of candidates) {
86
+ keyCandidates.push('.', './index.js', './index.mjs');
87
+ for (const key of keyCandidates) {
83
88
  if (!(key in exportsMap))
84
89
  continue;
85
90
  const entry = exportsMap[key];
91
+ // entry may be string or object like { import: "...", require: "..." }
86
92
  let target;
87
- if (typeof entry === 'string')
93
+ if (typeof entry === 'string') {
88
94
  target = entry;
95
+ }
89
96
  else if (entry && typeof entry === 'object') {
90
- const obj = entry;
91
- if (typeof obj.import === 'string')
92
- target = obj.import;
93
- else if (typeof obj.default === 'string')
94
- target = obj.default;
97
+ const entryObj = entry;
98
+ // Prefer "import" field for ESM consumers, then "default", then any string-ish value
99
+ if (typeof entryObj.import === 'string')
100
+ target = entryObj.import;
101
+ else if (typeof entryObj.default === 'string')
102
+ target = entryObj.default;
95
103
  else {
96
- for (const k of Object.keys(obj)) {
97
- if (typeof obj[k] === 'string') {
98
- target = obj[k];
104
+ // If the entry object itself is a conditional map (like {"node": "...", "browser": "..."}),
105
+ // attempt to pick any string value present.
106
+ for (const k of Object.keys(entryObj)) {
107
+ if (typeof entryObj[k] === 'string') {
108
+ target = entryObj[k];
99
109
  break;
100
110
  }
101
111
  }
102
112
  }
103
113
  }
104
- if (!target)
114
+ if (!target || typeof target !== 'string')
105
115
  continue;
116
+ // Normalize relative paths in exports (remove leading ./)
106
117
  const normalized = target.replace(/^\.\//, '');
107
- const abs = path.isAbsolute(normalized) ? normalized : path.resolve(pkgDir, normalized);
108
- if (await fs.pathExists(abs))
118
+ const abs = path.isAbsolute(normalized)
119
+ ? normalized
120
+ : path.resolve(pkgDir, normalized);
121
+ if (await fs.pathExists(abs)) {
109
122
  return abs;
123
+ }
110
124
  }
111
125
  }
112
126
  }
113
- // try package subpath resolution
127
+ // Try resolved subpath directly (pkg/subpath)
114
128
  if (subPath) {
115
129
  try {
116
- return require.resolve(`${pkgRoot}/${subPath}`, { paths: [root] });
130
+ const candidate = require.resolve(`${pkgRoot}/${subPath}`, { paths: [root] });
131
+ return candidate;
117
132
  }
118
133
  catch {
119
- // check common candidates under package dir
120
- const cand = [
134
+ // fallback to searching common candidates under package dir
135
+ const candPaths = [
121
136
  path.join(pkgDir, subPath),
122
- path.join(pkgDir, `${subPath}.js`),
123
- path.join(pkgDir, `${subPath}.mjs`),
137
+ path.join(pkgDir, subPath + '.js'),
138
+ path.join(pkgDir, subPath + '.mjs'),
124
139
  path.join(pkgDir, subPath, 'index.js'),
125
140
  path.join(pkgDir, subPath, 'index.mjs'),
126
141
  ];
127
- for (const c of cand) {
142
+ for (const c of candPaths) {
128
143
  if (await fs.pathExists(c))
129
144
  return c;
130
145
  }
131
146
  }
132
147
  }
133
- // check common package fields
134
- const maybeFields = [
148
+ // Try package's main/module/browser fields safely (typed as string)
149
+ const candidateFields = [
135
150
  typeof pkgJson.module === 'string' ? pkgJson.module : undefined,
136
151
  typeof pkgJson.browser === 'string' ? pkgJson.browser : undefined,
137
152
  typeof pkgJson.main === 'string' ? pkgJson.main : undefined,
138
153
  ];
139
- for (const f of maybeFields) {
140
- if (!f)
154
+ for (const field of candidateFields) {
155
+ if (!field)
141
156
  continue;
142
- const abs = path.isAbsolute(f) ? f : path.resolve(pkgDir, f);
157
+ const abs = path.isAbsolute(field) ? field : path.resolve(pkgDir, field);
143
158
  if (await fs.pathExists(abs))
144
159
  return abs;
145
160
  }
146
- throw new Error(`Could not resolve module entry for "${id}"`);
161
+ throw new Error(`Could not resolve module entry for ${id}`);
147
162
  }
163
+ /**
164
+ * Wrap the built module for subpath imports:
165
+ * For requests like "/@modules/react-dom/client" — we bundle the resolved file
166
+ * and return it. If the user requested the package root instead, the resolved
167
+ * bundle is returned directly.
168
+ *
169
+ * No hardcoded special cases.
170
+ */
148
171
  function normalizeCacheKey(id) {
149
172
  return id.replace(/[\\/]/g, '_');
150
173
  }
@@ -152,31 +175,39 @@ export default async function dev() {
152
175
  const root = process.cwd();
153
176
  const userConfig = (await loadReactClientConfig(root));
154
177
  const appRoot = path.resolve(root, userConfig.root || '.');
155
- const defaultPort = userConfig.server?.port ?? 2202;
178
+ const defaultPort = Number(process.env.PORT) || userConfig.server?.port || 2202;
179
+ // cache dir for prebundled deps
156
180
  const cacheDir = path.join(appRoot, '.react-client', 'deps');
157
181
  await fs.ensureDir(cacheDir);
158
- // Detect entry
159
- const possibleEntries = ['src/main.tsx', 'src/main.jsx'].map((p) => path.join(appRoot, p));
160
- const entry = possibleEntries.find((p) => fs.existsSync(p));
182
+ // Detect entry (main.tsx / main.jsx)
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));
161
190
  if (!entry) {
162
- 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/'));
163
192
  process.exit(1);
164
193
  }
165
- const indexHtml = path.join(appRoot, 'index.html');
166
- // Port selection
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');
204
+ // Select port
167
205
  const availablePort = await detectPort(defaultPort);
168
206
  const port = availablePort;
169
207
  if (availablePort !== defaultPort) {
170
- const res = await prompts({
171
- type: 'confirm',
172
- name: 'useNewPort',
173
- message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
174
- initial: true,
175
- });
176
- if (!res.useNewPort)
177
- process.exit(0);
208
+ console.log(chalk.yellow(`\n⚠️ Port ${defaultPort} is occupied. Using ${availablePort} instead.`));
178
209
  }
179
- // ensure react-refresh runtime present (templates often import it)
210
+ // Ensure react-refresh runtime available (used by many templates)
180
211
  try {
181
212
  require.resolve('react-refresh/runtime');
182
213
  }
@@ -189,10 +220,10 @@ export default async function dev() {
189
220
  });
190
221
  }
191
222
  catch {
192
- console.warn(chalk.yellow('⚠️ auto-install failed — please install react-refresh manually.'));
223
+ console.warn(chalk.yellow('⚠️ automatic install of react-refresh failed; continuing without it.'));
193
224
  }
194
225
  }
195
- // Core plugin(s)
226
+ // Plugin system (core + user)
196
227
  const corePlugins = [
197
228
  {
198
229
  name: 'css-hmr',
@@ -201,93 +232,285 @@ export default async function dev() {
201
232
  const escaped = JSON.stringify(code);
202
233
  return `
203
234
  const css = ${escaped};
204
- const style = document.createElement('style');
235
+ const style = document.createElement("style");
205
236
  style.textContent = css;
206
237
  document.head.appendChild(style);
207
- if (import.meta.hot) import.meta.hot.accept();
238
+ import.meta.hot?.accept();
208
239
  `;
209
240
  }
210
241
  return code;
211
242
  },
212
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
+ },
213
275
  ];
214
276
  const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
215
277
  const plugins = [...corePlugins, ...userPlugins];
216
- // app + caches
278
+ // App + caches
217
279
  const app = connect();
218
280
  const transformCache = new Map();
219
- // dependency analyzer for prebundling
220
- async function analyzeGraph(file, seen = new Set()) {
221
- if (seen.has(file))
222
- return seen;
223
- seen.add(file);
224
- try {
225
- const code = await fs.readFile(file, 'utf8');
226
- const matches = [
227
- ...code.matchAll(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g),
228
- ...code.matchAll(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g),
229
- ];
230
- for (const m of matches) {
231
- const dep = m[1];
232
- if (!dep || dep.startsWith('.') || dep.startsWith('/'))
233
- continue;
234
- try {
235
- const resolved = require.resolve(dep, { paths: [appRoot] });
236
- await analyzeGraph(resolved, seen);
237
- }
238
- catch {
239
- seen.add(dep);
281
+ // Helper: recursively analyze dependency graph for prebundling (bare imports)
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
+ }
240
313
  }
241
314
  }
315
+ catch {
316
+ // skip missing files
317
+ }
242
318
  }
243
- catch {
244
- // ignore
245
- }
246
- return seen;
319
+ await walk(file);
320
+ return deps;
247
321
  }
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
248
340
  async function prebundleDeps(deps) {
249
341
  if (!deps.size)
250
342
  return;
251
- const cached = new Set((await fs.readdir(cacheDir)).map((f) => f.replace(/\.js$/, '')));
252
- const missing = [...deps].filter((d) => !cached.has(d.replace(/\//g, '_')));
253
- if (!missing.length)
254
- return;
255
- console.log(chalk.cyan('📦 Prebundling:'), missing.join(', '));
256
- 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) {
257
349
  try {
258
- const entryPoint = require.resolve(dep, { paths: [appRoot] });
259
- const outFile = path.join(cacheDir, dep.replace(/\//g, '_') + '.js');
260
- await esbuild.build({
261
- entryPoints: [entryPoint],
262
- bundle: true,
263
- platform: 'browser',
264
- format: 'esm',
265
- outfile: outFile,
266
- write: true,
267
- target: ['es2020'],
268
- });
269
- 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;
270
432
  }
271
433
  catch (err) {
272
- console.warn(chalk.yellow(`⚠️ Skipped ${dep}: ${err.message}`));
434
+ console.warn(chalk.yellow(`⚠️ Could not resolve ${dep}: ${err.message}`));
273
435
  }
274
- }));
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
+ }
275
463
  }
276
- // initial prebundle
464
+ // Build initial prebundle graph from entry
277
465
  const depsSet = await analyzeGraph(entry);
466
+ // Ensure react/jsx-runtime is prebundled if used
467
+ depsSet.add('react/jsx-runtime');
278
468
  await prebundleDeps(depsSet);
279
- // re-prebundle on package.json change
469
+ // Watch package.json for changes to re-prebundle
280
470
  const pkgPath = path.join(appRoot, 'package.json');
281
471
  if (await fs.pathExists(pkgPath)) {
282
472
  chokidar.watch(pkgPath).on('change', async () => {
283
473
  console.log(chalk.yellow('📦 package.json changed — rebuilding prebundle...'));
284
474
  const newDeps = await analyzeGraph(entry);
475
+ newDeps.add('react/jsx-runtime');
285
476
  await prebundleDeps(newDeps);
286
477
  });
287
478
  }
288
- // --- Serve /@modules/
479
+ // --- Serve /@modules/<dep> (prebundled or on-demand esbuild bundle)
289
480
  app.use((async (req, res, next) => {
290
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
+ }
291
514
  if (!url.startsWith('/@modules/'))
292
515
  return next();
293
516
  const id = url.replace(/^\/@modules\//, '');
@@ -296,12 +519,25 @@ export default async function dev() {
296
519
  return res.end('// invalid module');
297
520
  }
298
521
  try {
299
- 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 = '';
300
530
  if (await fs.pathExists(cacheFile)) {
531
+ foundCacheFile = cacheFile;
532
+ }
533
+ else if (await fs.pathExists(cacheFileAlternative)) {
534
+ foundCacheFile = cacheFileAlternative;
535
+ }
536
+ if (foundCacheFile) {
301
537
  res.setHeader('Content-Type', jsContentType());
302
- return res.end(await fs.readFile(cacheFile, 'utf8'));
538
+ return res.end(await fs.readFile(foundCacheFile, 'utf8'));
303
539
  }
304
- // Resolve real entry (handles exports + subpaths)
540
+ // 2. Resolve the actual entry file for bare imports
305
541
  const entryFile = await resolveModuleEntry(id, appRoot);
306
542
  const result = await esbuild.build({
307
543
  entryPoints: [entryFile],
@@ -310,8 +546,15 @@ export default async function dev() {
310
546
  format: 'esm',
311
547
  write: false,
312
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
+ },
313
555
  });
314
556
  const output = result.outputFiles?.[0]?.text ?? '';
557
+ // Write cache and respond
315
558
  await fs.writeFile(cacheFile, output, 'utf8');
316
559
  res.setHeader('Content-Type', jsContentType());
317
560
  res.end(output);
@@ -321,27 +564,15 @@ export default async function dev() {
321
564
  res.end(`// Failed to resolve module ${id}: ${err.message}`);
322
565
  }
323
566
  }));
324
- // --- Serve overlay runtime: prefer local src/runtime/overlay-runtime.js else inline fallback
325
- const localOverlayPath = path.join(appRoot, 'src', 'runtime', 'overlay-runtime.js');
326
- app.use(async (req, res, next) => {
327
- if (req.url !== RUNTIME_OVERLAY_ROUTE)
328
- return next();
329
- try {
330
- if (await fs.pathExists(localOverlayPath)) {
331
- res.setHeader('Content-Type', jsContentType());
332
- return res.end(await fs.readFile(localOverlayPath, 'utf8'));
333
- }
334
- // Inline fallback runtime (minimal)
335
- const inlineRuntime = `
336
- /* Inline overlay fallback (auto-generated) */
337
- ${(() => {
338
- return `
567
+ // --- Serve runtime overlay (inline, no external dependencies)
568
+ const OVERLAY_RUNTIME = `
339
569
  const overlayId = "__rc_error_overlay__";
340
- (function(){
341
- const style = document.createElement('style');
570
+ (function(){
571
+ const style = document.createElement("style");
342
572
  style.textContent = \`
343
573
  #\${overlayId}{position:fixed;inset:0;background:rgba(0,0,0,0.9);color:#fff;font-family:Menlo,Consolas,monospace;font-size:14px;z-index:999999;overflow:auto;padding:24px;}
344
574
  #\${overlayId} h2{color:#ff6b6b;margin-bottom:16px;}
575
+ #\${overlayId} pre{background:rgba(255,255,255,0.06);padding:12px;border-radius:6px;overflow:auto;}
345
576
  .frame-file{color:#ffa500;cursor:pointer;font-weight:bold;margin-bottom:4px;}
346
577
  .line-number{opacity:0.6;margin-right:10px;display:inline-block;width:2em;text-align:right;}
347
578
  \`;
@@ -349,61 +580,65 @@ const overlayId = "__rc_error_overlay__";
349
580
  async function mapStackFrame(frame){
350
581
  const m = frame.match(/(\\/src\\/[^\s:]+):(\\d+):(\\d+)/);
351
582
  if(!m) return frame;
352
- const [,file,line] = m;
583
+ const [,file,line,col] = m;
353
584
  try{
354
- const resp = await fetch(\`/@source-map?file=\${file}&line=\${line}\`);
585
+ const resp = await fetch(\`/@source-map?file=\${file}&line=\${line}&column=\${col}\`);
355
586
  if(!resp.ok) return frame;
356
587
  const pos = await resp.json();
357
588
  if(pos.source) return pos;
358
589
  }catch(e){}
359
590
  return frame;
360
591
  }
592
+ function highlightSimple(s){
593
+ return s.replace(/(const|let|var|function|return|import|from|export|class|new|await|async|if|else|for|while|try|catch|throw)/g,'<span style="color:#ffb86c">$1</span>');
594
+ }
361
595
  async function renderOverlay(err){
362
596
  const overlay = document.getElementById(overlayId) || document.body.appendChild(Object.assign(document.createElement("div"),{id:overlayId}));
363
597
  overlay.innerHTML = "";
364
598
  const title = document.createElement("h2");
365
599
  title.textContent = "🔥 " + (err.message || "Error");
366
600
  overlay.appendChild(title);
367
- const frames = (err.stack||"").split("\\n").filter(l=>/src\\//.test(l));
368
- for(const f of frames){
369
- const mapped = await mapStackFrame(f);
370
- if(typeof mapped === 'string') continue;
601
+ const frames = (err.stack||"").split("\\n").filter(l => /src\\//.test(l));
602
+ for(const frame of frames){
603
+ const mapped = await mapStackFrame(frame);
604
+ if(typeof mapped === "string") continue;
605
+ const frameEl = document.createElement("div");
371
606
  const link = document.createElement("div");
372
607
  link.className = "frame-file";
373
608
  link.textContent = \`\${mapped.source||mapped.file}:\${mapped.line}:\${mapped.column}\`;
374
- overlay.appendChild(link);
609
+ link.onclick = ()=>window.open("vscode://file/"+(mapped.source||mapped.file)+":"+mapped.line);
610
+ frameEl.appendChild(link);
375
611
  if(mapped.snippet){
376
612
  const pre = document.createElement("pre");
377
- pre.innerHTML = mapped.snippet;
378
- overlay.appendChild(pre);
613
+ pre.innerHTML = highlightSimple(mapped.snippet);
614
+ frameEl.appendChild(pre);
379
615
  }
616
+ overlay.appendChild(frameEl);
380
617
  }
381
618
  }
382
619
  window.showErrorOverlay = (err)=>renderOverlay(err);
383
620
  window.clearErrorOverlay = ()=>document.getElementById(overlayId)?.remove();
384
- window.addEventListener("error", e=>window.showErrorOverlay?.(e.error||e));
385
- window.addEventListener("unhandledrejection", e=>window.showErrorOverlay?.(e.reason||e));
621
+ window.addEventListener("error", e => window.showErrorOverlay?.(e.error || e));
622
+ window.addEventListener("unhandledrejection", e => window.showErrorOverlay?.(e.reason || e));
386
623
  })();
387
624
  `;
388
- })()}
389
- `;
625
+ app.use((async (req, res, next) => {
626
+ if (req.url === '/@runtime/overlay') {
390
627
  res.setHeader('Content-Type', jsContentType());
391
- return res.end(inlineRuntime);
392
- }
393
- catch (err) {
394
- res.writeHead(500);
395
- res.end(`// overlay serve error: ${err.message}`);
628
+ return res.end(OVERLAY_RUNTIME);
396
629
  }
397
- });
398
- // --- /@source-map: return snippet around requested line
630
+ next();
631
+ }));
632
+ // --- minimal /@source-map: return snippet around requested line of original source file
399
633
  app.use((async (req, res, next) => {
400
634
  const url = req.url ?? '';
401
635
  if (!url.startsWith('/@source-map'))
402
636
  return next();
403
637
  try {
404
- const parsed = new URL(url, `http://localhost:${port}`);
638
+ const parsed = new URL(req.url ?? '', `http://localhost:${port}`);
405
639
  const file = parsed.searchParams.get('file') ?? '';
406
- const lineNum = Number(parsed.searchParams.get('line') ?? '0') || 0;
640
+ const lineStr = parsed.searchParams.get('line') ?? '0';
641
+ const lineNum = Number(lineStr) || 0;
407
642
  if (!file) {
408
643
  res.writeHead(400);
409
644
  return res.end('{}');
@@ -427,63 +662,87 @@ const overlayId = "__rc_error_overlay__";
427
662
  })
428
663
  .join('\n');
429
664
  res.setHeader('Content-Type', 'application/json');
430
- res.end(JSON.stringify({ source: file, line: lineNum, column: 0, snippet }));
665
+ res.end(JSON.stringify({ source: filePath, line: lineNum, column: 0, snippet }));
431
666
  }
432
667
  catch (err) {
433
668
  res.writeHead(500);
434
669
  res.end(JSON.stringify({ error: err.message }));
435
670
  }
436
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
+ }));
437
696
  // --- Serve /src/* files (on-the-fly transform + bare import rewrite)
438
697
  app.use((async (req, res, next) => {
439
698
  const url = req.url ?? '';
440
699
  if (!url.startsWith('/src/') && !url.endsWith('.css'))
441
700
  return next();
442
701
  const raw = decodeURIComponent((req.url ?? '').split('?')[0]);
443
- const filePathBase = path.join(appRoot, raw.replace(/^\//, ''));
702
+ const filePath = path.join(appRoot, raw.replace(/^\//, ''));
703
+ // Try file extensions if not exact file
444
704
  const exts = ['', '.tsx', '.ts', '.jsx', '.js', '.css'];
445
705
  let found = '';
446
706
  for (const ext of exts) {
447
- if (await fs.pathExists(filePathBase + ext)) {
448
- found = filePathBase + ext;
707
+ if (await fs.pathExists(filePath + ext)) {
708
+ found = filePath + ext;
449
709
  break;
450
710
  }
451
711
  }
452
712
  if (!found)
453
713
  return next();
454
714
  try {
455
- // cached transform
456
- if (transformCache.has(found)) {
457
- res.setHeader('Content-Type', jsContentType());
458
- res.end(transformCache.get(found));
459
- return;
460
- }
461
715
  let code = await fs.readFile(found, 'utf8');
462
- // rewrite bare imports to /@modules/*
463
- code = code
464
- .replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
465
- .replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
466
- // plugin transforms
716
+ // run plugin transforms
467
717
  for (const p of plugins) {
468
718
  if (p.onTransform) {
469
- // allow plugin transform to return string
470
- // eslint-disable-next-line no-await-in-loop
471
719
  const out = await p.onTransform(code, found);
472
720
  if (typeof out === 'string')
473
721
  code = out;
474
722
  }
475
723
  }
476
- // loader
477
724
  const ext = path.extname(found).toLowerCase();
478
725
  const loader = ext === '.ts' ? 'ts' : ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'js';
479
726
  const result = await esbuild.transform(code, {
480
727
  loader,
481
728
  sourcemap: 'inline',
482
729
  target: ['es2020'],
730
+ jsx: 'automatic',
483
731
  });
484
- 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);
485
744
  res.setHeader('Content-Type', jsContentType());
486
- res.end(result.code);
745
+ res.end(transformedCode);
487
746
  }
488
747
  catch (err) {
489
748
  const e = err;
@@ -491,7 +750,7 @@ const overlayId = "__rc_error_overlay__";
491
750
  res.end(`// transform error: ${e.message}`);
492
751
  }
493
752
  }));
494
- // --- Serve index.html and inject overlay + HMR client (if not already)
753
+ // --- Serve index.html with overlay + HMR client injection
495
754
  app.use((async (req, res, next) => {
496
755
  const url = req.url ?? '';
497
756
  if (url !== '/' && url !== '/index.html')
@@ -501,9 +760,25 @@ const overlayId = "__rc_error_overlay__";
501
760
  return res.end('index.html not found');
502
761
  }
503
762
  try {
504
- let html = await fs.readFile(indexHtml, 'utf8');
505
- if (!html.includes(RUNTIME_OVERLAY_ROUTE)) {
506
- 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
+ };
507
782
  const ws = new WebSocket("ws://" + location.host);
508
783
  ws.onmessage = (e) => {
509
784
  const msg = JSON.parse(e.data);
@@ -511,45 +786,55 @@ const overlayId = "__rc_error_overlay__";
511
786
  if (msg.type === "error") window.showErrorOverlay?.(msg);
512
787
  if (msg.type === "update") {
513
788
  window.clearErrorOverlay?.();
514
- 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
+ }
515
801
  }
516
802
  };
517
- </script>\n</body>`);
518
- }
803
+ </script>`.trim();
804
+ // Inject preamble at the top of <body>
805
+ const newHtml = html.replace('<body>', `<body>\n${reactRefreshPreamble}`);
519
806
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
520
- res.end(html);
807
+ res.end(newHtml);
521
808
  }
522
809
  catch (err) {
523
810
  res.writeHead(500);
524
811
  res.end(`// html read error: ${err.message}`);
525
812
  }
526
813
  }));
527
- // HMR WebSocket + BroadcastManager
814
+ // --- HMR WebSocket server
528
815
  const server = http.createServer(app);
529
816
  const broadcaster = new BroadcastManager(server);
530
- // Watcher for src plugin onHotUpdate + broadcast update
817
+ // Watch files and trigger plugin onHotUpdate + broadcast HMR message
531
818
  const watcher = chokidar.watch(path.join(appRoot, 'src'), { ignoreInitial: true });
532
819
  watcher.on('change', async (file) => {
533
820
  transformCache.delete(file);
821
+ // plugin hook onHotUpdate optionally
534
822
  for (const p of plugins) {
535
823
  if (p.onHotUpdate) {
536
824
  try {
537
- // plugin receives broadcast helper
538
- // cast to PluginHotUpdateContext (safe wrapper)
539
- // eslint-disable-next-line no-await-in-loop
540
825
  await p.onHotUpdate(file, {
541
- broadcast: (m) => {
542
- broadcaster.broadcast(m);
826
+ // plugin only needs broadcast in most cases
827
+ broadcast: (msg) => {
828
+ broadcaster.broadcast(msg);
543
829
  },
544
830
  });
545
831
  }
546
832
  catch (err) {
547
- // log plugin error but continue
548
- // eslint-disable-next-line no-console
549
833
  console.warn('plugin onHotUpdate error:', err.message);
550
834
  }
551
835
  }
552
836
  }
837
+ // default: broadcast update for changed file
553
838
  broadcaster.broadcast({
554
839
  type: 'update',
555
840
  path: '/' + path.relative(appRoot, file).replace(/\\/g, '/'),
@@ -559,43 +844,32 @@ const overlayId = "__rc_error_overlay__";
559
844
  server.listen(port, async () => {
560
845
  const url = `http://localhost:${port}`;
561
846
  console.log(chalk.cyan.bold('\n🚀 React Client Dev Server'));
562
- console.log(chalk.gray('──────────────────────────────'));
563
847
  console.log(chalk.green(`⚡ Running at: ${url}`));
564
- // open if not explicitly disabled. using safe cast to allow unknown shape of userConfig.server
565
- const shouldOpen = userConfig.server?.open !== false;
566
- if (shouldOpen) {
848
+ if (userConfig.server?.open !== false) {
567
849
  try {
568
850
  await open(url);
569
851
  }
570
852
  catch {
571
- /* ignore */
572
- }
573
- }
574
- const ctx = {
575
- root: appRoot,
576
- outDir: cacheDir,
577
- app,
578
- wss: broadcaster.wss,
579
- httpServer: server,
580
- broadcast: (m) => broadcaster.broadcast(m),
581
- };
582
- // plugin serve/start hooks
583
- for (const p of plugins) {
584
- if (p.onServe) {
585
- await p.onServe(ctx);
586
- }
587
- if (p.onServerStart) {
588
- await p.onServerStart(ctx);
853
+ // ignore open errors
589
854
  }
590
855
  }
591
856
  });
592
857
  // graceful shutdown
593
- process.on('SIGINT', async () => {
594
- console.log(chalk.red('\n🛑 Shutting down...'));
595
- watcher.close();
596
- broadcaster.close();
597
- server.close();
598
- process.exit(0);
599
- });
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);
600
874
  }
601
875
  //# sourceMappingURL=dev.js.map