react-client 1.0.30 → 1.0.32

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.
@@ -9,15 +9,6 @@
9
9
  *
10
10
  * Keep this file linted & typed. Avoids manual react-dom/client hacks.
11
11
  */
12
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
13
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
14
- return new (P || (P = Promise))(function (resolve, reject) {
15
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
16
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
17
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
18
- step((generator = generator.apply(thisArg, _arguments || [])).next());
19
- });
20
- };
21
12
  import esbuild from 'esbuild';
22
13
  import connect from 'connect';
23
14
  import http from 'http';
@@ -29,9 +20,14 @@ import fs from 'fs-extra';
29
20
  import open from 'open';
30
21
  import chalk from 'chalk';
31
22
  import { execSync } from 'child_process';
32
- import { loadReactClientConfig } from '../../utils/loadConfig';
33
23
  import { BroadcastManager } from '../../server/broadcastManager';
34
24
  import { createRequire } from 'module';
25
+ import { fileURLToPath } from 'url';
26
+ import { dirname, resolve } from 'path';
27
+ const __filename = fileURLToPath(import.meta.url);
28
+ const __dirname = dirname(__filename);
29
+ const loadConfigPath = resolve(__dirname, '../../utils/loadConfig.js');
30
+ const { loadReactClientConfig } = await import(loadConfigPath);
35
31
  const require = createRequire(import.meta.url);
36
32
  const RUNTIME_OVERLAY_ROUTE = '/@runtime/overlay';
37
33
  function jsContentType() {
@@ -44,130 +40,128 @@ function jsContentType() {
44
40
  * 3. try package.json exports field
45
41
  * 4. try common fallback candidates
46
42
  */
47
- function resolveModuleEntry(id, root) {
48
- return __awaiter(this, void 0, void 0, function* () {
49
- // quick resolution
50
- try {
51
- return require.resolve(id, { paths: [root] });
52
- }
53
- catch (_a) {
54
- // continue
55
- }
56
- // split package root and subpath
57
- const parts = id.split('/');
58
- const pkgRoot = parts[0].startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
59
- const subPath = parts.slice(pkgRoot.startsWith('@') ? 2 : 1).join('/');
60
- let pkgJsonPath;
61
- try {
62
- pkgJsonPath = require.resolve(`${pkgRoot}/package.json`, { paths: [root] });
63
- }
64
- catch (_b) {
65
- // No need to keep unused variable 'err'
66
- throw new Error(`Package not found: ${pkgRoot}`);
67
- }
68
- const pkgDir = path.dirname(pkgJsonPath);
69
- // Explicitly type pkgJson to avoid 'any'
70
- let pkgJson = {};
71
- try {
72
- const pkgContent = yield fs.readFile(pkgJsonPath, 'utf8');
73
- pkgJson = JSON.parse(pkgContent);
74
- }
75
- catch (_c) {
76
- // ignore parse or read errors gracefully
43
+ async function resolveModuleEntry(id, root) {
44
+ // quick resolution
45
+ try {
46
+ return require.resolve(id, { paths: [root] });
47
+ }
48
+ catch {
49
+ // continue
50
+ }
51
+ // split package root and subpath
52
+ const parts = id.split('/');
53
+ const pkgRoot = parts[0].startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
54
+ const subPath = parts.slice(pkgRoot.startsWith('@') ? 2 : 1).join('/');
55
+ let pkgJsonPath;
56
+ try {
57
+ pkgJsonPath = require.resolve(`${pkgRoot}/package.json`, { paths: [root] });
58
+ }
59
+ catch {
60
+ // No need to keep unused variable 'err'
61
+ throw new Error(`Package not found: ${pkgRoot}`);
62
+ }
63
+ const pkgDir = path.dirname(pkgJsonPath);
64
+ // Explicitly type pkgJson to avoid 'any'
65
+ let pkgJson = {};
66
+ try {
67
+ const pkgContent = await fs.readFile(pkgJsonPath, 'utf8');
68
+ pkgJson = JSON.parse(pkgContent);
69
+ }
70
+ catch {
71
+ // ignore parse or read errors gracefully
72
+ }
73
+ // If exports field exists, try to look up subpath (type-safe, supports conditional exports)
74
+ if (pkgJson.exports) {
75
+ const exportsField = pkgJson.exports;
76
+ // If exports is a plain string -> it's the entry
77
+ if (typeof exportsField === 'string') {
78
+ if (!subPath)
79
+ return path.resolve(pkgDir, exportsField);
77
80
  }
78
- // If exports field exists, try to look up subpath (type-safe, supports conditional exports)
79
- if (pkgJson.exports) {
80
- const exportsField = pkgJson.exports;
81
- // If exports is a plain string -> it's the entry
82
- if (typeof exportsField === 'string') {
83
- if (!subPath)
84
- return path.resolve(pkgDir, exportsField);
81
+ else if (exportsField && typeof exportsField === 'object') {
82
+ // Normalize to a record so we can index it safely
83
+ const exportsMap = exportsField;
84
+ // Try candidates in order: explicit subpath, index, fallback
85
+ const keyCandidates = [];
86
+ if (subPath) {
87
+ keyCandidates.push(`./${subPath}`, `./${subPath}.js`, `./${subPath}.mjs`);
85
88
  }
86
- else if (exportsField && typeof exportsField === 'object') {
87
- // Normalize to a record so we can index it safely
88
- const exportsMap = exportsField;
89
- // Try candidates in order: explicit subpath, index, fallback
90
- const keyCandidates = [];
91
- if (subPath) {
92
- keyCandidates.push(`./${subPath}`, `./${subPath}.js`, `./${subPath}.mjs`);
89
+ keyCandidates.push('.', './index.js', './index.mjs');
90
+ for (const key of keyCandidates) {
91
+ if (!(key in exportsMap))
92
+ continue;
93
+ const entry = exportsMap[key];
94
+ // entry may be string or object like { import: "...", require: "..." }
95
+ let target;
96
+ if (typeof entry === 'string') {
97
+ target = entry;
93
98
  }
94
- keyCandidates.push('.', './index.js', './index.mjs');
95
- for (const key of keyCandidates) {
96
- if (!(key in exportsMap))
97
- continue;
98
- const entry = exportsMap[key];
99
- // entry may be string or object like { import: "...", require: "..." }
100
- let target;
101
- if (typeof entry === 'string') {
102
- target = entry;
103
- }
104
- else if (entry && typeof entry === 'object') {
105
- const entryObj = entry;
106
- // Prefer "import" field for ESM consumers, then "default", then any string-ish value
107
- if (typeof entryObj.import === 'string')
108
- target = entryObj.import;
109
- else if (typeof entryObj.default === 'string')
110
- target = entryObj.default;
111
- else {
112
- // If the entry object itself is a conditional map (like {"node": "...", "browser": "..."}),
113
- // attempt to pick any string value present.
114
- for (const k of Object.keys(entryObj)) {
115
- if (typeof entryObj[k] === 'string') {
116
- target = entryObj[k];
117
- break;
118
- }
99
+ else if (entry && typeof entry === 'object') {
100
+ const entryObj = entry;
101
+ // Prefer "import" field for ESM consumers, then "default", then any string-ish value
102
+ if (typeof entryObj.import === 'string')
103
+ target = entryObj.import;
104
+ else if (typeof entryObj.default === 'string')
105
+ target = entryObj.default;
106
+ else {
107
+ // If the entry object itself is a conditional map (like {"node": "...", "browser": "..."}),
108
+ // attempt to pick any string value present.
109
+ for (const k of Object.keys(entryObj)) {
110
+ if (typeof entryObj[k] === 'string') {
111
+ target = entryObj[k];
112
+ break;
119
113
  }
120
114
  }
121
115
  }
122
- if (!target || typeof target !== 'string')
123
- continue;
124
- // Normalize relative paths in exports (remove leading ./)
125
- const normalized = target.replace(/^\.\//, '');
126
- const abs = path.isAbsolute(normalized)
127
- ? normalized
128
- : path.resolve(pkgDir, normalized);
129
- if (yield fs.pathExists(abs)) {
130
- return abs;
131
- }
132
116
  }
133
- }
134
- }
135
- // Try resolved subpath directly (pkg/subpath)
136
- if (subPath) {
137
- try {
138
- const candidate = require.resolve(`${pkgRoot}/${subPath}`, { paths: [root] });
139
- return candidate;
140
- }
141
- catch (_d) {
142
- // fallback to searching common candidates under package dir
143
- const candPaths = [
144
- path.join(pkgDir, subPath),
145
- path.join(pkgDir, subPath + '.js'),
146
- path.join(pkgDir, subPath + '.mjs'),
147
- path.join(pkgDir, subPath, 'index.js'),
148
- path.join(pkgDir, subPath, 'index.mjs'),
149
- ];
150
- for (const c of candPaths) {
151
- if (yield fs.pathExists(c))
152
- return c;
117
+ if (!target || typeof target !== 'string')
118
+ continue;
119
+ // Normalize relative paths in exports (remove leading ./)
120
+ const normalized = target.replace(/^\.\//, '');
121
+ const abs = path.isAbsolute(normalized)
122
+ ? normalized
123
+ : path.resolve(pkgDir, normalized);
124
+ if (await fs.pathExists(abs)) {
125
+ return abs;
153
126
  }
154
127
  }
155
128
  }
156
- // Try package's main/module/browser fields safely (typed as string)
157
- const candidateFields = [
158
- typeof pkgJson.module === 'string' ? pkgJson.module : undefined,
159
- typeof pkgJson.browser === 'string' ? pkgJson.browser : undefined,
160
- typeof pkgJson.main === 'string' ? pkgJson.main : undefined,
161
- ];
162
- for (const field of candidateFields) {
163
- if (!field)
164
- continue;
165
- const abs = path.isAbsolute(field) ? field : path.resolve(pkgDir, field);
166
- if (yield fs.pathExists(abs))
167
- return abs;
129
+ }
130
+ // Try resolved subpath directly (pkg/subpath)
131
+ if (subPath) {
132
+ try {
133
+ const candidate = require.resolve(`${pkgRoot}/${subPath}`, { paths: [root] });
134
+ return candidate;
168
135
  }
169
- throw new Error(`Could not resolve module entry for ${id}`);
170
- });
136
+ catch {
137
+ // fallback to searching common candidates under package dir
138
+ const candPaths = [
139
+ path.join(pkgDir, subPath),
140
+ path.join(pkgDir, subPath + '.js'),
141
+ path.join(pkgDir, subPath + '.mjs'),
142
+ path.join(pkgDir, subPath, 'index.js'),
143
+ path.join(pkgDir, subPath, 'index.mjs'),
144
+ ];
145
+ for (const c of candPaths) {
146
+ if (await fs.pathExists(c))
147
+ return c;
148
+ }
149
+ }
150
+ }
151
+ // Try package's main/module/browser fields safely (typed as string)
152
+ const candidateFields = [
153
+ typeof pkgJson.module === 'string' ? pkgJson.module : undefined,
154
+ typeof pkgJson.browser === 'string' ? pkgJson.browser : undefined,
155
+ typeof pkgJson.main === 'string' ? pkgJson.main : undefined,
156
+ ];
157
+ for (const field of candidateFields) {
158
+ if (!field)
159
+ continue;
160
+ const abs = path.isAbsolute(field) ? field : path.resolve(pkgDir, field);
161
+ if (await fs.pathExists(abs))
162
+ return abs;
163
+ }
164
+ throw new Error(`Could not resolve module entry for ${id}`);
171
165
  }
172
166
  /**
173
167
  * Wrap the built module for subpath imports:
@@ -180,201 +174,192 @@ function resolveModuleEntry(id, root) {
180
174
  function normalizeCacheKey(id) {
181
175
  return id.replace(/[\\/]/g, '_');
182
176
  }
183
- export default function dev() {
184
- return __awaiter(this, void 0, void 0, function* () {
185
- var _a, _b;
186
- const root = process.cwd();
187
- const userConfig = (yield loadReactClientConfig(root));
188
- const appRoot = path.resolve(root, userConfig.root || '.');
189
- const defaultPort = (_b = (_a = userConfig.server) === null || _a === void 0 ? void 0 : _a.port) !== null && _b !== void 0 ? _b : 2202;
190
- // cache dir for prebundled deps
191
- const cacheDir = path.join(appRoot, '.react-client', 'deps');
192
- yield fs.ensureDir(cacheDir);
193
- // Detect entry (main.tsx / main.jsx)
194
- const possible = ['src/main.tsx', 'src/main.jsx'].map((p) => path.join(appRoot, p));
195
- const entry = possible.find((p) => fs.existsSync(p));
196
- if (!entry) {
197
- console.error(chalk.red('āŒ Entry not found: src/main.tsx or src/main.jsx'));
198
- process.exit(1);
199
- }
200
- const indexHtml = path.join(appRoot, 'index.html');
201
- // Select port
202
- const availablePort = yield detectPort(defaultPort);
203
- const port = availablePort;
204
- if (availablePort !== defaultPort) {
205
- const response = yield prompts({
206
- type: 'confirm',
207
- name: 'useNewPort',
208
- message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
209
- initial: true,
210
- });
211
- if (!response.useNewPort) {
212
- console.log('šŸ›‘ Dev server cancelled.');
213
- process.exit(0);
214
- }
177
+ export default async function dev() {
178
+ const root = process.cwd();
179
+ const userConfig = (await loadReactClientConfig(root));
180
+ const appRoot = path.resolve(root, userConfig.root || '.');
181
+ const defaultPort = userConfig.server?.port ?? 2202;
182
+ // cache dir for prebundled deps
183
+ const cacheDir = path.join(appRoot, '.react-client', 'deps');
184
+ await fs.ensureDir(cacheDir);
185
+ // 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));
188
+ if (!entry) {
189
+ console.error(chalk.red('āŒ Entry not found: src/main.tsx or src/main.jsx'));
190
+ process.exit(1);
191
+ }
192
+ const indexHtml = path.join(appRoot, 'index.html');
193
+ // Select port
194
+ const availablePort = await detectPort(defaultPort);
195
+ const port = availablePort;
196
+ 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);
215
206
  }
216
- // Ensure react-refresh runtime available (used by many templates)
207
+ }
208
+ // Ensure react-refresh runtime available (used by many templates)
209
+ try {
210
+ require.resolve('react-refresh/runtime');
211
+ }
212
+ catch {
213
+ console.warn(chalk.yellow('āš ļø react-refresh not found — installing react-refresh...'));
217
214
  try {
218
- require.resolve('react-refresh/runtime');
215
+ execSync('npm install react-refresh --no-audit --no-fund --silent', {
216
+ cwd: appRoot,
217
+ stdio: 'inherit',
218
+ });
219
219
  }
220
- catch (_c) {
221
- console.warn(chalk.yellow('āš ļø react-refresh not found — installing react-refresh...'));
222
- try {
223
- execSync('npm install react-refresh --no-audit --no-fund --silent', {
224
- cwd: appRoot,
225
- stdio: 'inherit',
226
- });
227
- }
228
- catch (_d) {
229
- console.warn(chalk.yellow('āš ļø automatic install of react-refresh failed; continuing without it.'));
230
- }
220
+ catch {
221
+ console.warn(chalk.yellow('āš ļø automatic install of react-refresh failed; continuing without it.'));
231
222
  }
232
- // Plugin system (core + user)
233
- const corePlugins = [
234
- {
235
- name: 'css-hmr',
236
- onTransform(code, id) {
237
- return __awaiter(this, void 0, void 0, function* () {
238
- if (id.endsWith('.css')) {
239
- const escaped = JSON.stringify(code);
240
- return `
223
+ }
224
+ // Plugin system (core + user)
225
+ const corePlugins = [
226
+ {
227
+ name: 'css-hmr',
228
+ async onTransform(code, id) {
229
+ if (id.endsWith('.css')) {
230
+ const escaped = JSON.stringify(code);
231
+ return `
241
232
  const css = ${escaped};
242
233
  const style = document.createElement("style");
243
234
  style.textContent = css;
244
235
  document.head.appendChild(style);
245
236
  import.meta.hot?.accept();
246
237
  `;
247
- }
248
- return code;
249
- });
250
- },
238
+ }
239
+ return code;
251
240
  },
252
- ];
253
- const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
254
- const plugins = [...corePlugins, ...userPlugins];
255
- // App + caches
256
- const app = connect();
257
- const transformCache = new Map();
258
- // Helper: recursively analyze dependency graph for prebundling (bare imports)
259
- function analyzeGraph(file_1) {
260
- return __awaiter(this, arguments, void 0, function* (file, seen = new Set()) {
261
- if (seen.has(file))
262
- return seen;
263
- seen.add(file);
241
+ },
242
+ ];
243
+ const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
244
+ const plugins = [...corePlugins, ...userPlugins];
245
+ // App + caches
246
+ const app = connect();
247
+ const transformCache = new Map();
248
+ // 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;
264
263
  try {
265
- const code = yield fs.readFile(file, 'utf8');
266
- const matches = [
267
- ...code.matchAll(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g),
268
- ...code.matchAll(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g),
269
- ];
270
- for (const m of matches) {
271
- const dep = m[1];
272
- if (!dep || dep.startsWith('.') || dep.startsWith('/'))
273
- continue;
274
- try {
275
- const resolved = require.resolve(dep, { paths: [appRoot] });
276
- yield analyzeGraph(resolved, seen);
277
- }
278
- catch (_a) {
279
- // bare dependency (node_modules) - track name
280
- seen.add(dep);
281
- }
282
- }
264
+ const resolved = require.resolve(dep, { paths: [appRoot] });
265
+ await analyzeGraph(resolved, seen);
283
266
  }
284
- catch (_b) {
285
- // ignore unreadable files
267
+ catch {
268
+ // bare dependency (node_modules) - track name
269
+ seen.add(dep);
286
270
  }
287
- return seen;
288
- });
289
- }
290
- // Prebundle dependencies into cache dir (parallel)
291
- function prebundleDeps(deps) {
292
- return __awaiter(this, void 0, void 0, function* () {
293
- if (!deps.size)
294
- return;
295
- const existingFiles = yield fs.readdir(cacheDir);
296
- const existing = new Set(existingFiles.map((f) => f.replace(/\.js$/, '')));
297
- const missing = [...deps].filter((d) => !existing.has(d));
298
- if (!missing.length)
299
- return;
300
- console.log(chalk.cyan('šŸ“¦ Prebundling:'), missing.join(', '));
301
- yield Promise.all(missing.map((dep) => __awaiter(this, void 0, void 0, function* () {
302
- try {
303
- const entryPoint = require.resolve(dep, { paths: [appRoot] });
304
- const outFile = path.join(cacheDir, normalizeCacheKey(dep) + '.js');
305
- yield esbuild.build({
306
- entryPoints: [entryPoint],
307
- bundle: true,
308
- platform: 'browser',
309
- format: 'esm',
310
- outfile: outFile,
311
- write: true,
312
- target: ['es2020'],
313
- });
314
- console.log(chalk.green(`āœ… Cached ${dep}`));
315
- }
316
- catch (err) {
317
- console.warn(chalk.yellow(`āš ļø Skipped ${dep}: ${err.message}`));
318
- }
319
- })));
320
- });
271
+ }
321
272
  }
322
- // Build initial prebundle graph from entry
323
- const depsSet = yield analyzeGraph(entry);
324
- yield prebundleDeps(depsSet);
325
- // Watch package.json for changes to re-prebundle
326
- const pkgPath = path.join(appRoot, 'package.json');
327
- if (yield fs.pathExists(pkgPath)) {
328
- chokidar.watch(pkgPath).on('change', () => __awaiter(this, void 0, void 0, function* () {
329
- console.log(chalk.yellow('šŸ“¦ package.json changed — rebuilding prebundle...'));
330
- const newDeps = yield analyzeGraph(entry);
331
- yield prebundleDeps(newDeps);
332
- }));
273
+ catch {
274
+ // ignore unreadable files
333
275
  }
334
- // --- Serve /@modules/<dep> (prebundled or on-demand esbuild bundle)
335
- app.use(((req, res, next) => __awaiter(this, void 0, void 0, function* () {
336
- var _a, _b, _c, _d;
337
- const url = (_a = req.url) !== null && _a !== void 0 ? _a : '';
338
- if (!url.startsWith('/@modules/'))
339
- return next();
340
- const id = url.replace(/^\/@modules\//, '');
341
- if (!id) {
342
- res.writeHead(400);
343
- return res.end('// invalid module');
344
- }
276
+ return seen;
277
+ }
278
+ // Prebundle dependencies into cache dir (parallel)
279
+ async function prebundleDeps(deps) {
280
+ if (!deps.size)
281
+ 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) => {
345
289
  try {
346
- const cacheFile = path.join(cacheDir, normalizeCacheKey(id) + '.js');
347
- if (yield fs.pathExists(cacheFile)) {
348
- res.setHeader('Content-Type', jsContentType());
349
- return res.end(yield fs.readFile(cacheFile, 'utf8'));
350
- }
351
- // Resolve the actual entry file (handles subpaths & package exports)
352
- const entryFile = yield resolveModuleEntry(id, appRoot);
353
- const result = yield esbuild.build({
354
- entryPoints: [entryFile],
290
+ const entryPoint = require.resolve(dep, { paths: [appRoot] });
291
+ const outFile = path.join(cacheDir, normalizeCacheKey(dep) + '.js');
292
+ await esbuild.build({
293
+ entryPoints: [entryPoint],
355
294
  bundle: true,
356
295
  platform: 'browser',
357
296
  format: 'esm',
358
- write: false,
297
+ outfile: outFile,
298
+ write: true,
359
299
  target: ['es2020'],
360
300
  });
361
- const output = (_d = (_c = (_b = result.outputFiles) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.text) !== null && _d !== void 0 ? _d : '';
362
- // Write cache and respond
363
- yield fs.writeFile(cacheFile, output, 'utf8');
364
- res.setHeader('Content-Type', jsContentType());
365
- res.end(output);
301
+ console.log(chalk.green(`āœ… Cached ${dep}`));
366
302
  }
367
303
  catch (err) {
368
- res.writeHead(500);
369
- res.end(`// Failed to resolve module ${id}: ${err.message}`);
304
+ console.warn(chalk.yellow(`āš ļø Skipped ${dep}: ${err.message}`));
370
305
  }
371
- })));
372
- // --- Serve runtime overlay (inline, no external dependencies)
373
- const OVERLAY_RUNTIME = `
306
+ }));
307
+ }
308
+ // Build initial prebundle graph from entry
309
+ const depsSet = await analyzeGraph(entry);
310
+ await prebundleDeps(depsSet);
311
+ // Watch package.json for changes to re-prebundle
312
+ const pkgPath = path.join(appRoot, 'package.json');
313
+ if (await fs.pathExists(pkgPath)) {
314
+ chokidar.watch(pkgPath).on('change', async () => {
315
+ console.log(chalk.yellow('šŸ“¦ package.json changed — rebuilding prebundle...'));
316
+ const newDeps = await analyzeGraph(entry);
317
+ await prebundleDeps(newDeps);
318
+ });
319
+ }
320
+ // --- Serve /@modules/<dep> (prebundled or on-demand esbuild bundle)
321
+ app.use((async (req, res, next) => {
322
+ const url = req.url ?? '';
323
+ if (!url.startsWith('/@modules/'))
324
+ return next();
325
+ const id = url.replace(/^\/@modules\//, '');
326
+ if (!id) {
327
+ res.writeHead(400);
328
+ return res.end('// invalid module');
329
+ }
330
+ try {
331
+ const cacheFile = path.join(cacheDir, normalizeCacheKey(id) + '.js');
332
+ if (await fs.pathExists(cacheFile)) {
333
+ res.setHeader('Content-Type', jsContentType());
334
+ return res.end(await fs.readFile(cacheFile, 'utf8'));
335
+ }
336
+ // Resolve the actual entry file (handles subpaths & package exports)
337
+ const entryFile = await resolveModuleEntry(id, appRoot);
338
+ const result = await esbuild.build({
339
+ entryPoints: [entryFile],
340
+ bundle: true,
341
+ platform: 'browser',
342
+ format: 'esm',
343
+ write: false,
344
+ target: ['es2020'],
345
+ });
346
+ const output = result.outputFiles?.[0]?.text ?? '';
347
+ // Write cache and respond
348
+ await fs.writeFile(cacheFile, output, 'utf8');
349
+ res.setHeader('Content-Type', jsContentType());
350
+ res.end(output);
351
+ }
352
+ catch (err) {
353
+ res.writeHead(500);
354
+ res.end(`// Failed to resolve module ${id}: ${err.message}`);
355
+ }
356
+ }));
357
+ // --- Serve runtime overlay (inline, no external dependencies)
358
+ const OVERLAY_RUNTIME = `
374
359
  /* inline overlay runtime - served at ${RUNTIME_OVERLAY_ROUTE} */
375
360
  ${(() => {
376
- // small helper — embed as a string
377
- return `
361
+ // small helper — embed as a string
362
+ return `
378
363
  const overlayId = "__rc_error_overlay__";
379
364
  (function(){
380
365
  const style = document.createElement("style");
@@ -435,122 +420,119 @@ const overlayId = "__rc_error_overlay__";
435
420
  window.addEventListener("unhandledrejection", e => window.showErrorOverlay?.(e.reason || e));
436
421
  })();
437
422
  `;
438
- })()}
423
+ })()}
439
424
  `;
440
- app.use((req, res, next) => __awaiter(this, void 0, void 0, function* () {
441
- if (req.url === RUNTIME_OVERLAY_ROUTE) {
442
- res.setHeader('Content-Type', jsContentType());
443
- return res.end(OVERLAY_RUNTIME);
444
- }
445
- next();
446
- }));
447
- // --- minimal /@source-map: return snippet around requested line of original source file
448
- app.use(((req, res, next) => __awaiter(this, void 0, void 0, function* () {
449
- var _a, _b, _c, _d;
450
- const url = (_a = req.url) !== null && _a !== void 0 ? _a : '';
451
- if (!url.startsWith('/@source-map'))
452
- return next();
453
- try {
454
- const parsed = new URL((_b = req.url) !== null && _b !== void 0 ? _b : '', `http://localhost:${port}`);
455
- const file = (_c = parsed.searchParams.get('file')) !== null && _c !== void 0 ? _c : '';
456
- const lineStr = (_d = parsed.searchParams.get('line')) !== null && _d !== void 0 ? _d : '0';
457
- const lineNum = Number(lineStr) || 0;
458
- if (!file) {
459
- res.writeHead(400);
460
- return res.end('{}');
461
- }
462
- const filePath = path.join(appRoot, file.startsWith('/') ? file.slice(1) : file);
463
- if (!(yield fs.pathExists(filePath))) {
464
- res.writeHead(404);
465
- return res.end('{}');
466
- }
467
- const src = yield fs.readFile(filePath, 'utf8');
468
- const lines = src.split(/\r?\n/);
469
- const start = Math.max(0, lineNum - 3 - 1);
470
- const end = Math.min(lines.length, lineNum + 2);
471
- const snippet = lines
472
- .slice(start, end)
473
- .map((l, i) => {
474
- const ln = start + i + 1;
475
- return `<span class="line-number">${ln}</span> ${l
476
- .replace(/</g, '&lt;')
477
- .replace(/>/g, '&gt;')}`;
478
- })
479
- .join('\n');
480
- res.setHeader('Content-Type', 'application/json');
481
- res.end(JSON.stringify({ source: file, line: lineNum, column: 0, snippet }));
425
+ app.use(async (req, res, next) => {
426
+ if (req.url === RUNTIME_OVERLAY_ROUTE) {
427
+ res.setHeader('Content-Type', jsContentType());
428
+ return res.end(OVERLAY_RUNTIME);
429
+ }
430
+ next();
431
+ });
432
+ // --- minimal /@source-map: return snippet around requested line of original source file
433
+ app.use((async (req, res, next) => {
434
+ const url = req.url ?? '';
435
+ if (!url.startsWith('/@source-map'))
436
+ return next();
437
+ try {
438
+ const parsed = new URL(req.url ?? '', `http://localhost:${port}`);
439
+ const file = parsed.searchParams.get('file') ?? '';
440
+ const lineStr = parsed.searchParams.get('line') ?? '0';
441
+ const lineNum = Number(lineStr) || 0;
442
+ if (!file) {
443
+ res.writeHead(400);
444
+ return res.end('{}');
482
445
  }
483
- catch (err) {
484
- res.writeHead(500);
485
- res.end(JSON.stringify({ error: err.message }));
446
+ const filePath = path.join(appRoot, file.startsWith('/') ? file.slice(1) : file);
447
+ if (!(await fs.pathExists(filePath))) {
448
+ res.writeHead(404);
449
+ return res.end('{}');
486
450
  }
487
- })));
488
- // --- Serve /src/* files (on-the-fly transform + bare import rewrite)
489
- app.use(((req, res, next) => __awaiter(this, void 0, void 0, function* () {
490
- var _a, _b;
491
- const url = (_a = req.url) !== null && _a !== void 0 ? _a : '';
492
- if (!url.startsWith('/src/') && !url.endsWith('.css'))
493
- return next();
494
- const raw = decodeURIComponent(((_b = req.url) !== null && _b !== void 0 ? _b : '').split('?')[0]);
495
- const filePath = path.join(appRoot, raw.replace(/^\//, ''));
496
- // Try file extensions if not exact file
497
- const exts = ['', '.tsx', '.ts', '.jsx', '.js', '.css'];
498
- let found = '';
499
- for (const ext of exts) {
500
- if (yield fs.pathExists(filePath + ext)) {
501
- found = filePath + ext;
502
- break;
503
- }
451
+ const src = await fs.readFile(filePath, 'utf8');
452
+ const lines = src.split(/\r?\n/);
453
+ const start = Math.max(0, lineNum - 3 - 1);
454
+ const end = Math.min(lines.length, lineNum + 2);
455
+ const snippet = lines
456
+ .slice(start, end)
457
+ .map((l, i) => {
458
+ const ln = start + i + 1;
459
+ return `<span class="line-number">${ln}</span> ${l
460
+ .replace(/</g, '&lt;')
461
+ .replace(/>/g, '&gt;')}`;
462
+ })
463
+ .join('\n');
464
+ res.setHeader('Content-Type', 'application/json');
465
+ res.end(JSON.stringify({ source: file, line: lineNum, column: 0, snippet }));
466
+ }
467
+ catch (err) {
468
+ res.writeHead(500);
469
+ res.end(JSON.stringify({ error: err.message }));
470
+ }
471
+ }));
472
+ // --- Serve /src/* files (on-the-fly transform + bare import rewrite)
473
+ app.use((async (req, res, next) => {
474
+ const url = req.url ?? '';
475
+ if (!url.startsWith('/src/') && !url.endsWith('.css'))
476
+ return next();
477
+ const raw = decodeURIComponent((req.url ?? '').split('?')[0]);
478
+ const filePath = path.join(appRoot, raw.replace(/^\//, ''));
479
+ // Try file extensions if not exact file
480
+ const exts = ['', '.tsx', '.ts', '.jsx', '.js', '.css'];
481
+ let found = '';
482
+ for (const ext of exts) {
483
+ if (await fs.pathExists(filePath + ext)) {
484
+ found = filePath + ext;
485
+ break;
504
486
  }
505
- if (!found)
506
- return next();
507
- try {
508
- let code = yield fs.readFile(found, 'utf8');
509
- // rewrite bare imports -> /@modules/<dep>
510
- code = code
511
- .replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
512
- .replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
513
- // run plugin transforms
514
- for (const p of plugins) {
515
- if (p.onTransform) {
516
- const out = yield p.onTransform(code, found);
517
- if (typeof out === 'string')
518
- code = out;
519
- }
487
+ }
488
+ if (!found)
489
+ return next();
490
+ try {
491
+ let code = await fs.readFile(found, 'utf8');
492
+ // rewrite bare imports -> /@modules/<dep>
493
+ code = code
494
+ .replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
495
+ .replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
496
+ // run plugin transforms
497
+ for (const p of plugins) {
498
+ if (p.onTransform) {
499
+ const out = await p.onTransform(code, found);
500
+ if (typeof out === 'string')
501
+ code = out;
520
502
  }
521
- // choose loader by extension
522
- const ext = path.extname(found).toLowerCase();
523
- const loader = ext === '.ts' ? 'ts' : ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'js';
524
- const result = yield esbuild.transform(code, {
525
- loader,
526
- sourcemap: 'inline',
527
- target: ['es2020'],
528
- });
529
- transformCache.set(found, result.code);
530
- res.setHeader('Content-Type', jsContentType());
531
- res.end(result.code);
532
- }
533
- catch (err) {
534
- const e = err;
535
- res.writeHead(500);
536
- res.end(`// transform error: ${e.message}`);
537
503
  }
538
- })));
539
- // --- Serve index.html with overlay + HMR client injection
540
- app.use(((req, res, next) => __awaiter(this, void 0, void 0, function* () {
541
- var _a;
542
- const url = (_a = req.url) !== null && _a !== void 0 ? _a : '';
543
- if (url !== '/' && url !== '/index.html')
544
- return next();
545
- if (!(yield fs.pathExists(indexHtml))) {
546
- res.writeHead(404);
547
- return res.end('index.html not found');
548
- }
549
- try {
550
- let html = yield fs.readFile(indexHtml, 'utf8');
551
- // inject overlay runtime and HMR client if not already present
552
- if (!html.includes(RUNTIME_OVERLAY_ROUTE)) {
553
- html = html.replace('</body>', `\n<script type="module" src="${RUNTIME_OVERLAY_ROUTE}"></script>\n<script type="module">
504
+ // choose loader by extension
505
+ const ext = path.extname(found).toLowerCase();
506
+ const loader = ext === '.ts' ? 'ts' : ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'js';
507
+ const result = await esbuild.transform(code, {
508
+ loader,
509
+ sourcemap: 'inline',
510
+ target: ['es2020'],
511
+ });
512
+ transformCache.set(found, result.code);
513
+ res.setHeader('Content-Type', jsContentType());
514
+ res.end(result.code);
515
+ }
516
+ catch (err) {
517
+ const e = err;
518
+ res.writeHead(500);
519
+ res.end(`// transform error: ${e.message}`);
520
+ }
521
+ }));
522
+ // --- Serve index.html with overlay + HMR client injection
523
+ app.use((async (req, res, next) => {
524
+ const url = req.url ?? '';
525
+ if (url !== '/' && url !== '/index.html')
526
+ return next();
527
+ if (!(await fs.pathExists(indexHtml))) {
528
+ res.writeHead(404);
529
+ return res.end('index.html not found');
530
+ }
531
+ try {
532
+ let html = await fs.readFile(indexHtml, 'utf8');
533
+ // inject overlay runtime and HMR client if not already present
534
+ if (!html.includes(RUNTIME_OVERLAY_ROUTE)) {
535
+ html = html.replace('</body>', `\n<script type="module" src="${RUNTIME_OVERLAY_ROUTE}"></script>\n<script type="module">
554
536
  const ws = new WebSocket("ws://" + location.host);
555
537
  ws.onmessage = (e) => {
556
538
  const msg = JSON.parse(e.data);
@@ -562,66 +544,64 @@ const overlayId = "__rc_error_overlay__";
562
544
  }
563
545
  };
564
546
  </script>\n</body>`);
565
- }
566
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
567
- res.end(html);
568
- }
569
- catch (err) {
570
- res.writeHead(500);
571
- res.end(`// html read error: ${err.message}`);
572
- }
573
- })));
574
- // --- HMR WebSocket server
575
- const server = http.createServer(app);
576
- const broadcaster = new BroadcastManager(server);
577
- // Watch files and trigger plugin onHotUpdate + broadcast HMR message
578
- const watcher = chokidar.watch(path.join(appRoot, 'src'), { ignoreInitial: true });
579
- watcher.on('change', (file) => __awaiter(this, void 0, void 0, function* () {
580
- transformCache.delete(file);
581
- // plugin hook onHotUpdate optionally
582
- for (const p of plugins) {
583
- if (p.onHotUpdate) {
584
- try {
585
- yield p.onHotUpdate(file, {
586
- // plugin only needs broadcast in most cases
587
- broadcast: (msg) => {
588
- broadcaster.broadcast(msg);
589
- },
590
- });
591
- }
592
- catch (err) {
593
- console.warn('plugin onHotUpdate error:', err.message);
594
- }
595
- }
596
547
  }
597
- // default: broadcast update for changed file
598
- broadcaster.broadcast({
599
- type: 'update',
600
- path: '/' + path.relative(appRoot, file).replace(/\\/g, '/'),
601
- });
602
- }));
603
- // start server
604
- server.listen(port, () => __awaiter(this, void 0, void 0, function* () {
605
- var _a;
606
- const url = `http://localhost:${port}`;
607
- console.log(chalk.cyan.bold('\nšŸš€ React Client Dev Server'));
608
- console.log(chalk.green(`⚔ Running at: ${url}`));
609
- if (((_a = userConfig.server) === null || _a === void 0 ? void 0 : _a.open) !== false) {
548
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
549
+ res.end(html);
550
+ }
551
+ catch (err) {
552
+ res.writeHead(500);
553
+ res.end(`// html read error: ${err.message}`);
554
+ }
555
+ }));
556
+ // --- HMR WebSocket server
557
+ const server = http.createServer(app);
558
+ const broadcaster = new BroadcastManager(server);
559
+ // Watch files and trigger plugin onHotUpdate + broadcast HMR message
560
+ const watcher = chokidar.watch(path.join(appRoot, 'src'), { ignoreInitial: true });
561
+ watcher.on('change', async (file) => {
562
+ transformCache.delete(file);
563
+ // plugin hook onHotUpdate optionally
564
+ for (const p of plugins) {
565
+ if (p.onHotUpdate) {
610
566
  try {
611
- yield open(url);
567
+ await p.onHotUpdate(file, {
568
+ // plugin only needs broadcast in most cases
569
+ broadcast: (msg) => {
570
+ broadcaster.broadcast(msg);
571
+ },
572
+ });
612
573
  }
613
- catch (_b) {
614
- // ignore open errors
574
+ catch (err) {
575
+ console.warn('plugin onHotUpdate error:', err.message);
615
576
  }
616
577
  }
617
- }));
618
- // graceful shutdown
619
- process.on('SIGINT', () => __awaiter(this, void 0, void 0, function* () {
620
- console.log(chalk.red('\nšŸ›‘ Shutting down...'));
621
- yield watcher.close();
622
- broadcaster.close();
623
- server.close();
624
- process.exit(0);
625
- }));
578
+ }
579
+ // default: broadcast update for changed file
580
+ broadcaster.broadcast({
581
+ type: 'update',
582
+ path: '/' + path.relative(appRoot, file).replace(/\\/g, '/'),
583
+ });
584
+ });
585
+ // start server
586
+ server.listen(port, async () => {
587
+ const url = `http://localhost:${port}`;
588
+ console.log(chalk.cyan.bold('\nšŸš€ React Client Dev Server'));
589
+ console.log(chalk.green(`⚔ Running at: ${url}`));
590
+ if (userConfig.server?.open !== false) {
591
+ try {
592
+ await open(url);
593
+ }
594
+ catch {
595
+ // ignore open errors
596
+ }
597
+ }
598
+ });
599
+ // graceful shutdown
600
+ process.on('SIGINT', async () => {
601
+ console.log(chalk.red('\nšŸ›‘ Shutting down...'));
602
+ await watcher.close();
603
+ broadcaster.close();
604
+ server.close();
605
+ process.exit(0);
626
606
  });
627
607
  }