react-client 1.0.28 → 1.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,5 @@
1
- "use strict";
2
1
  /**
3
- * dev.ts — Vite-like dev server for react-client
2
+ * dev.ts — dev server for react-client
4
3
  *
5
4
  * - prebundles deps into .react-client/deps
6
5
  * - serves /@modules/<dep>
@@ -10,434 +9,548 @@
10
9
  *
11
10
  * Keep this file linted & typed. Avoids manual react-dom/client hacks.
12
11
  */
13
- var __importDefault = (this && this.__importDefault) || function (mod) {
14
- return (mod && mod.__esModule) ? mod : { "default": mod };
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
+ });
15
20
  };
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.default = dev;
18
- const esbuild_1 = __importDefault(require("esbuild"));
19
- const connect_1 = __importDefault(require("connect"));
20
- const http_1 = __importDefault(require("http"));
21
- const chokidar_1 = __importDefault(require("chokidar"));
22
- const detect_port_1 = __importDefault(require("detect-port"));
23
- const prompts_1 = __importDefault(require("prompts"));
24
- const path_1 = __importDefault(require("path"));
25
- const fs_extra_1 = __importDefault(require("fs-extra"));
26
- const open_1 = __importDefault(require("open"));
27
- const chalk_1 = __importDefault(require("chalk"));
28
- const child_process_1 = require("child_process");
29
- const loadConfig_1 = require("../../utils/loadConfig");
30
- const broadcastManager_1 = require("../../server/broadcastManager");
31
- async function dev() {
32
- const root = process.cwd();
33
- const userConfig = (await (0, loadConfig_1.loadReactClientConfig)(root));
34
- const appRoot = path_1.default.resolve(root, userConfig.root || '.');
35
- const defaultPort = userConfig.server?.port ?? 2202;
36
- // cache dir for prebundled deps
37
- const cacheDir = path_1.default.join(appRoot, '.react-client', 'deps');
38
- await fs_extra_1.default.ensureDir(cacheDir);
39
- // Detect entry (main.tsx / main.jsx)
40
- const possible = ['src/main.tsx', 'src/main.jsx'].map((p) => path_1.default.join(appRoot, p));
41
- const entry = possible.find((p) => fs_extra_1.default.existsSync(p));
42
- if (!entry) {
43
- console.error(chalk_1.default.red('❌ Entry not found: src/main.tsx or src/main.jsx'));
44
- process.exit(1);
45
- }
46
- const indexHtml = path_1.default.join(appRoot, 'index.html');
47
- // Select port
48
- const availablePort = await (0, detect_port_1.default)(defaultPort);
49
- const port = availablePort;
50
- if (availablePort !== defaultPort) {
51
- const response = await (0, prompts_1.default)({
52
- type: 'confirm',
53
- name: 'useNewPort',
54
- message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
55
- initial: true,
56
- });
57
- if (!response.useNewPort) {
58
- console.log('🛑 Dev server cancelled.');
59
- process.exit(0);
21
+ import esbuild from 'esbuild';
22
+ import connect from 'connect';
23
+ import http from 'http';
24
+ import chokidar from 'chokidar';
25
+ import detectPort from 'detect-port';
26
+ import prompts from 'prompts';
27
+ import path from 'path';
28
+ import fs from 'fs-extra';
29
+ import open from 'open';
30
+ import chalk from 'chalk';
31
+ import { execSync } from 'child_process';
32
+ import { loadReactClientConfig } from '../../utils/loadConfig';
33
+ import { BroadcastManager } from '../../server/broadcastManager';
34
+ import { createRequire } from 'module';
35
+ const require = createRequire(import.meta.url);
36
+ const RUNTIME_OVERLAY_ROUTE = '/@runtime/overlay';
37
+ function jsContentType() {
38
+ return 'application/javascript; charset=utf-8';
39
+ }
40
+ /**
41
+ * Resolve any bare import id robustly:
42
+ * 1. try require.resolve(id)
43
+ * 2. try require.resolve(`${pkg}/${subpath}`)
44
+ * 3. try package.json exports field
45
+ * 4. try common fallback candidates
46
+ */
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] });
60
52
  }
61
- }
62
- // Ensure react-refresh runtime available (used by many templates)
63
- try {
64
- require.resolve('react-refresh/runtime');
65
- }
66
- catch {
67
- console.warn(chalk_1.default.yellow('⚠️ react-refresh not found — installing react-refresh...'));
68
- (0, child_process_1.execSync)('npm install react-refresh --no-audit --no-fund --silent', {
69
- cwd: appRoot,
70
- stdio: 'inherit',
71
- });
72
- }
73
- // Plugin system (core + user)
74
- const corePlugins = [
75
- {
76
- name: 'css-hmr',
77
- async onTransform(code, id) {
78
- if (id.endsWith('.css')) {
79
- const escaped = JSON.stringify(code);
80
- return `
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
77
+ }
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);
85
+ }
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`);
93
+ }
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
+ }
119
+ }
120
+ }
121
+ }
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
+ }
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;
153
+ }
154
+ }
155
+ }
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;
168
+ }
169
+ throw new Error(`Could not resolve module entry for ${id}`);
170
+ });
171
+ }
172
+ /**
173
+ * Wrap the built module for subpath imports:
174
+ * For requests like "/@modules/react-dom/client" — we bundle the resolved file
175
+ * and return it. If the user requested the package root instead, the resolved
176
+ * bundle is returned directly.
177
+ *
178
+ * No hardcoded special cases.
179
+ */
180
+ function normalizeCacheKey(id) {
181
+ return id.replace(/[\\/]/g, '_');
182
+ }
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
+ }
215
+ }
216
+ // Ensure react-refresh runtime available (used by many templates)
217
+ try {
218
+ require.resolve('react-refresh/runtime');
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
+ }
231
+ }
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 `
81
241
  const css = ${escaped};
82
242
  const style = document.createElement("style");
83
243
  style.textContent = css;
84
244
  document.head.appendChild(style);
85
245
  import.meta.hot?.accept();
86
246
  `;
87
- }
88
- return code;
247
+ }
248
+ return code;
249
+ });
250
+ },
89
251
  },
90
- },
91
- ];
92
- const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
93
- const plugins = [...corePlugins, ...userPlugins];
94
- // App + caches
95
- const app = (0, connect_1.default)();
96
- const transformCache = new Map();
97
- // Helper: recursively analyze dependency graph for prebundling (bare imports)
98
- async function analyzeGraph(file, seen = new Set()) {
99
- if (seen.has(file))
100
- return seen;
101
- seen.add(file);
102
- try {
103
- const code = await fs_extra_1.default.readFile(file, 'utf8');
104
- const matches = [
105
- ...code.matchAll(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g),
106
- ...code.matchAll(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g),
107
- ];
108
- for (const m of matches) {
109
- const dep = m[1];
110
- if (!dep || dep.startsWith('.') || dep.startsWith('/'))
111
- continue;
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);
112
264
  try {
113
- const resolved = require.resolve(dep, { paths: [appRoot] });
114
- await analyzeGraph(resolved, seen);
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
+ }
115
283
  }
116
- catch {
117
- // bare dependency (node_modules) - track name
118
- seen.add(dep);
284
+ catch (_b) {
285
+ // ignore unreadable files
119
286
  }
120
- }
287
+ return seen;
288
+ });
121
289
  }
122
- catch {
123
- // ignore unreadable files
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
+ });
124
321
  }
125
- return seen;
126
- }
127
- // Prebundle dependencies into cache dir (parallel)
128
- async function prebundleDeps(deps) {
129
- if (!deps.size)
130
- return;
131
- const existingFiles = await fs_extra_1.default.readdir(cacheDir);
132
- const existing = new Set(existingFiles.map((f) => f.replace(/\.js$/, '')));
133
- const missing = [...deps].filter((d) => !existing.has(d));
134
- if (!missing.length)
135
- return;
136
- console.log(chalk_1.default.cyan('📦 Prebundling:'), missing.join(', '));
137
- await Promise.all(missing.map(async (dep) => {
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
+ }));
333
+ }
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
+ }
138
345
  try {
139
- const entryPoint = require.resolve(dep, { paths: [appRoot] });
140
- const outFile = path_1.default.join(cacheDir, dep.replace(/\//g, '_') + '.js');
141
- await esbuild_1.default.build({
142
- entryPoints: [entryPoint],
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],
143
355
  bundle: true,
144
356
  platform: 'browser',
145
357
  format: 'esm',
146
- outfile: outFile,
147
- write: true,
358
+ write: false,
148
359
  target: ['es2020'],
149
360
  });
150
- console.log(chalk_1.default.green(`✅ Cached ${dep}`));
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);
151
366
  }
152
367
  catch (err) {
153
- console.warn(chalk_1.default.yellow(`⚠️ Skipped ${dep}: ${err.message}`));
154
- }
155
- }));
156
- }
157
- // Build initial prebundle graph from entry
158
- const depsSet = await analyzeGraph(entry);
159
- await prebundleDeps(depsSet);
160
- // Watch package.json for changes to re-prebundle
161
- const pkgPath = path_1.default.join(appRoot, 'package.json');
162
- if (await fs_extra_1.default.pathExists(pkgPath)) {
163
- chokidar_1.default.watch(pkgPath).on('change', async () => {
164
- console.log(chalk_1.default.yellow('📦 package.json changed — rebuilding prebundle...'));
165
- const newDeps = await analyzeGraph(entry);
166
- await prebundleDeps(newDeps);
167
- });
168
- }
169
- // --- Serve /@modules/<dep> (prebundled or on-demand esbuild bundle)
170
- app.use(async (req, res, next) => {
171
- const url = req.url ?? '';
172
- if (!url.startsWith('/@modules/'))
173
- return next();
174
- const id = url.replace(/^\/@modules\//, '');
175
- if (!id) {
176
- res.writeHead(400);
177
- return res.end('// invalid module');
178
- }
179
- try {
180
- const cacheFile = path_1.default.join(cacheDir, id.replace(/[\\/]/g, '_') + '.js');
181
- if (await fs_extra_1.default.pathExists(cacheFile)) {
182
- res.setHeader('Content-Type', 'application/javascript');
183
- return res.end(await fs_extra_1.default.readFile(cacheFile, 'utf8'));
184
- }
185
- // 🧠 Handle subpath imports correctly (like react-dom/client)
186
- let entryFile = null;
187
- try {
188
- entryFile = require.resolve(id, { paths: [appRoot] });
189
- }
190
- catch {
191
- const parts = id.split('/');
192
- const pkgRoot = parts[0].startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
193
- const subPath = parts.slice(pkgRoot.startsWith('@') ? 2 : 1).join('/');
194
- const pkgJsonPath = require.resolve(`${pkgRoot}/package.json`, { paths: [appRoot] });
195
- const pkgDir = path_1.default.dirname(pkgJsonPath);
196
- // Special case: react-dom/client
197
- if (pkgRoot === 'react-dom' && subPath === 'client') {
198
- entryFile = path_1.default.join(pkgDir, 'client.js');
199
- }
200
- else {
201
- const candidates = [
202
- path_1.default.join(pkgDir, subPath),
203
- path_1.default.join(pkgDir, subPath, 'index.js'),
204
- path_1.default.join(pkgDir, subPath + '.js'),
205
- path_1.default.join(pkgDir, subPath + '.mjs'),
206
- ];
207
- for (const f of candidates) {
208
- if (await fs_extra_1.default.pathExists(f)) {
209
- entryFile = f;
210
- break;
211
- }
212
- }
213
- }
368
+ res.writeHead(500);
369
+ res.end(`// Failed to resolve module ${id}: ${err.message}`);
214
370
  }
215
- if (!entryFile)
216
- throw new Error(`Cannot resolve module: ${id}`);
217
- const result = await esbuild_1.default.build({
218
- entryPoints: [entryFile],
219
- bundle: true,
220
- platform: 'browser',
221
- format: 'esm',
222
- write: false,
223
- target: ['es2020'],
224
- });
225
- const output = result.outputFiles?.[0]?.text ?? '';
226
- await fs_extra_1.default.writeFile(cacheFile, output, 'utf8');
227
- res.setHeader('Content-Type', 'application/javascript');
228
- res.end(output);
229
- }
230
- catch (err) {
231
- res.writeHead(500);
232
- res.end(`// Failed to resolve module ${id}: ${err.message}`);
233
- }
234
- });
235
- // --- Serve runtime overlay (inline, no external dependencies)
236
- const OVERLAY_RUNTIME = `
371
+ })));
372
+ // --- Serve runtime overlay (inline, no external dependencies)
373
+ const OVERLAY_RUNTIME = `
374
+ /* inline overlay runtime - served at ${RUNTIME_OVERLAY_ROUTE} */
375
+ ${(() => {
376
+ // small helper — embed as a string
377
+ return `
237
378
  const overlayId = "__rc_error_overlay__";
379
+ (function(){
380
+ const style = document.createElement("style");
381
+ style.textContent = \`
382
+ #\${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;}
383
+ #\${overlayId} h2{color:#ff6b6b;margin-bottom:16px;}
384
+ #\${overlayId} pre{background:rgba(255,255,255,0.06);padding:12px;border-radius:6px;overflow:auto;}
385
+ .frame-file{color:#ffa500;cursor:pointer;font-weight:bold;margin-bottom:4px;}
386
+ .line-number{opacity:0.6;margin-right:10px;display:inline-block;width:2em;text-align:right;}
387
+ \`;
388
+ document.head.appendChild(style);
238
389
 
239
- const style = document.createElement("style");
240
- style.textContent = \`
241
- #\${overlayId} {
242
- position: fixed;
243
- inset: 0;
244
- background: rgba(0, 0, 0, 0.9);
245
- color: #fff;
246
- font-family: Menlo, Consolas, monospace;
247
- font-size: 14px;
248
- z-index: 999999;
249
- overflow: auto;
250
- padding: 24px;
251
- animation: fadeIn 0.2s ease-out;
390
+ async function mapStackFrame(frame){
391
+ const m = frame.match(/(\\/src\\/[^\s:]+):(\\d+):(\\d+)/);
392
+ if(!m) return frame;
393
+ const [,file,line,col] = m;
394
+ try{
395
+ const resp = await fetch(\`/@source-map?file=\${file}&line=\${line}&column=\${col}\`);
396
+ if(!resp.ok) return frame;
397
+ const pos = await resp.json();
398
+ if(pos.source) return pos;
399
+ }catch(e){}
400
+ return frame;
252
401
  }
253
- @keyframes fadeIn { from {opacity: 0;} to {opacity: 1;} }
254
- #\${overlayId} h2 { color: #ff6b6b; margin-bottom: 16px; }
255
- #\${overlayId} pre { background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; overflow-x: auto; }
256
- #\${overlayId} a { color: #9cf; text-decoration: underline; }
257
- #\${overlayId} .frame { margin: 12px 0; }
258
- #\${overlayId} .frame-file { color: #ffa500; cursor: pointer; font-weight: bold; margin-bottom: 4px; }
259
- .line-number { opacity: 0.5; margin-right: 10px; display: inline-block; width: 2em; text-align: right; }
260
- \`;
261
- document.head.appendChild(style);
262
402
 
263
- async function mapStackFrame(frame) {
264
- const m = frame.match(/(\\/src\\/[^\s:]+):(\\d+):(\\d+)/);
265
- if (!m) return frame;
266
- const [, file, line, col] = m;
267
- const resp = await fetch(\`/@source-map?file=\${file}&line=\${line}&column=\${col}\`);
268
- if (!resp.ok) return frame;
269
- const pos = await resp.json();
270
- if (pos.source) {
271
- return {
272
- file: pos.source,
273
- line: pos.line,
274
- column: pos.column,
275
- snippet: pos.snippet || ""
276
- };
403
+ function highlightSimple(s){
404
+ 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>');
277
405
  }
278
- return frame;
279
- }
280
-
281
- // 🔹 minimal inline syntax highlighting (keywords only)
282
- function highlightJS(code) {
283
- return code
284
- .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>')
285
- .replace(/("[^"]*"|'[^']*')/g, '<span style="color:#8be9fd;">$1</span>')
286
- .replace(/(\\/\\/.*)/g, '<span style="opacity:0.6;">$1</span>');
287
- }
288
-
289
- async function renderOverlay(err) {
290
- const overlay =
291
- document.getElementById(overlayId) ||
292
- document.body.appendChild(Object.assign(document.createElement("div"), { id: overlayId }));
293
- overlay.innerHTML = "";
294
- const title = document.createElement("h2");
295
- title.textContent = "🔥 " + (err.message || "Error");
296
- overlay.appendChild(title);
297
406
 
298
- const frames = (err.stack || "").split("\\n").filter(l => /src\\//.test(l));
299
- for (const frame of frames) {
300
- const mapped = await mapStackFrame(frame);
301
- if (typeof mapped === "string") continue;
302
- const frameEl = document.createElement("div");
303
- frameEl.className = "frame";
304
-
305
- const link = document.createElement("div");
306
- link.className = "frame-file";
307
- link.textContent = \`\${mapped.file}:\${mapped.line}:\${mapped.column}\`;
308
- link.onclick = () =>
309
- window.open("vscode://file/" + location.origin.replace("http://", "") + mapped.file + ":" + mapped.line);
310
- frameEl.appendChild(link);
311
-
312
- if (mapped.snippet) {
313
- const pre = document.createElement("pre");
314
- pre.innerHTML = highlightJS(mapped.snippet);
315
- frameEl.appendChild(pre);
407
+ async function renderOverlay(err){
408
+ const overlay = document.getElementById(overlayId) || document.body.appendChild(Object.assign(document.createElement("div"),{id:overlayId}));
409
+ overlay.innerHTML = "";
410
+ const title = document.createElement("h2");
411
+ title.textContent = "🔥 " + (err.message || "Error");
412
+ overlay.appendChild(title);
413
+ const frames = (err.stack||"").split("\\n").filter(l => /src\\//.test(l));
414
+ for(const frame of frames){
415
+ const mapped = await mapStackFrame(frame);
416
+ if(typeof mapped === "string") continue;
417
+ const frameEl = document.createElement("div");
418
+ const link = document.createElement("div");
419
+ link.className = "frame-file";
420
+ link.textContent = \`\${mapped.source||mapped.file}:\${mapped.line}:\${mapped.column}\`;
421
+ link.onclick = ()=>window.open("vscode://file/"+(mapped.source||mapped.file)+":"+mapped.line);
422
+ frameEl.appendChild(link);
423
+ if(mapped.snippet){
424
+ const pre = document.createElement("pre");
425
+ pre.innerHTML = highlightSimple(mapped.snippet);
426
+ frameEl.appendChild(pre);
427
+ }
428
+ overlay.appendChild(frameEl);
316
429
  }
317
-
318
- overlay.appendChild(frameEl);
319
430
  }
320
- }
321
431
 
322
- window.showErrorOverlay = (err) => renderOverlay(err);
323
- window.clearErrorOverlay = () => document.getElementById(overlayId)?.remove();
432
+ window.showErrorOverlay = (err)=>renderOverlay(err);
433
+ window.clearErrorOverlay = ()=>document.getElementById(overlayId)?.remove();
434
+ window.addEventListener("error", e => window.showErrorOverlay?.(e.error || e));
435
+ window.addEventListener("unhandledrejection", e => window.showErrorOverlay?.(e.reason || e));
436
+ })();
324
437
  `;
325
- app.use(async (req, res, next) => {
326
- if (req.url === '/@runtime/overlay') {
327
- res.setHeader('Content-Type', 'application/javascript');
328
- return res.end(OVERLAY_RUNTIME);
329
- }
330
- next();
331
- });
332
- // --- minimal /@source-map: return snippet around requested line of original source file
333
- app.use((async (req, res, next) => {
334
- const url = req.url ?? '';
335
- if (!url.startsWith('/@source-map'))
336
- return next();
337
- // expected query: ?file=/src/xyz.tsx&line=12&column=3
338
- try {
339
- const full = req.url ?? '';
340
- const parsed = new URL(full, `http://localhost:${port}`);
341
- const file = parsed.searchParams.get('file') ?? '';
342
- const lineStr = parsed.searchParams.get('line') ?? '0';
343
- const lineNum = Number(lineStr) || 0;
344
- if (!file) {
345
- res.writeHead(400);
346
- return res.end('{}');
438
+ })()}
439
+ `;
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);
347
444
  }
348
- const filePath = path_1.default.join(appRoot, file.startsWith('/') ? file.slice(1) : file);
349
- if (!(await fs_extra_1.default.pathExists(filePath))) {
350
- res.writeHead(404);
351
- return res.end('{}');
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 }));
352
482
  }
353
- const src = await fs_extra_1.default.readFile(filePath, 'utf8');
354
- const lines = src.split(/\r?\n/);
355
- const start = Math.max(0, lineNum - 3 - 1);
356
- const end = Math.min(lines.length, lineNum + 2);
357
- const snippet = lines
358
- .slice(start, end)
359
- .map((l, i) => {
360
- const ln = start + i + 1;
361
- return `<span class="line-number">${ln}</span> ${l
362
- .replace(/</g, '&lt;')
363
- .replace(/>/g, '&gt;')}`;
364
- })
365
- .join('\n');
366
- res.setHeader('Content-Type', 'application/json');
367
- res.end(JSON.stringify({ source: file, line: lineNum, column: 0, snippet }));
368
- }
369
- catch (err) {
370
- res.writeHead(500);
371
- res.end(JSON.stringify({ error: err.message }));
372
- }
373
- }));
374
- // --- Serve /src/* files (on-the-fly transform + bare import rewrite)
375
- app.use((async (req, res, next) => {
376
- const url = req.url ?? '';
377
- if (!url.startsWith('/src/') && !url.endsWith('.css'))
378
- return next();
379
- const raw = decodeURIComponent((req.url ?? '').split('?')[0]);
380
- const filePath = path_1.default.join(appRoot, raw.replace(/^\//, ''));
381
- // Try file extensions if not exact file
382
- const exts = ['', '.tsx', '.ts', '.jsx', '.js', '.css'];
383
- let found = '';
384
- for (const ext of exts) {
385
- if (await fs_extra_1.default.pathExists(filePath + ext)) {
386
- found = filePath + ext;
387
- break;
483
+ catch (err) {
484
+ res.writeHead(500);
485
+ res.end(JSON.stringify({ error: err.message }));
388
486
  }
389
- }
390
- if (!found)
391
- return next();
392
- try {
393
- let code = await fs_extra_1.default.readFile(found, 'utf8');
394
- // rewrite bare imports -> /@modules/<dep>
395
- code = code
396
- .replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
397
- .replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
398
- // run plugin transforms
399
- for (const p of plugins) {
400
- if (p.onTransform) {
401
- // plugin may return transformed code
402
- // keep typed as string
403
- // eslint-disable-next-line no-await-in-loop
404
- const out = await p.onTransform(code, found);
405
- if (typeof out === 'string')
406
- code = out;
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;
407
503
  }
408
504
  }
409
- // choose loader by extension
410
- const ext = path_1.default.extname(found).toLowerCase();
411
- const loader = ext === '.ts' ? 'ts' : ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'js';
412
- const result = await esbuild_1.default.transform(code, {
413
- loader,
414
- sourcemap: 'inline',
415
- target: ['es2020'],
416
- });
417
- transformCache.set(found, result.code);
418
- res.setHeader('Content-Type', 'application/javascript');
419
- res.end(result.code);
420
- }
421
- catch (err) {
422
- const e = err;
423
- res.writeHead(500);
424
- res.end(`// transform error: ${e.message}`);
425
- }
426
- }));
427
- // --- Serve index.html with overlay + HMR client injection
428
- app.use((async (req, res, next) => {
429
- const url = req.url ?? '';
430
- if (url !== '/' && url !== '/index.html')
431
- return next();
432
- if (!(await fs_extra_1.default.pathExists(indexHtml))) {
433
- res.writeHead(404);
434
- return res.end('index.html not found');
435
- }
436
- try {
437
- let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
438
- // inject overlay runtime and HMR client if not already present
439
- if (!html.includes('/@runtime/overlay')) {
440
- html = html.replace('</body>', `\n<script type="module" src="/@runtime/overlay"></script>\n<script type="module">
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
+ }
520
+ }
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
+ }
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">
441
554
  const ws = new WebSocket("ws://" + location.host);
442
555
  ws.onmessage = (e) => {
443
556
  const msg = JSON.parse(e.data);
@@ -449,70 +562,66 @@ window.clearErrorOverlay = () => document.getElementById(overlayId)?.remove();
449
562
  }
450
563
  };
451
564
  </script>\n</body>`);
565
+ }
566
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
567
+ res.end(html);
452
568
  }
453
- res.setHeader('Content-Type', 'text/html');
454
- res.end(html);
455
- }
456
- catch (err) {
457
- res.writeHead(500);
458
- res.end(`// html read error: ${err.message}`);
459
- }
460
- }));
461
- // --- HMR WebSocket server
462
- const server = http_1.default.createServer(app);
463
- const broadcaster = new broadcastManager_1.BroadcastManager(server);
464
- // Watch files and trigger plugin onHotUpdate + broadcast HMR message
465
- const watcher = chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true });
466
- watcher.on('change', async (file) => {
467
- transformCache.delete(file);
468
- // plugin hook onHotUpdate optionally
469
- for (const p of plugins) {
470
- if (p.onHotUpdate) {
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
+ }
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) {
471
610
  try {
472
- // allow plugin to broadcast via a simple function
473
- // plugin gets { broadcast }
474
- // plugin signature: onHotUpdate(file, { broadcast })
475
- // eslint-disable-next-line no-await-in-loop
476
- await p.onHotUpdate(file, {
477
- broadcast: (msg) => {
478
- broadcaster.broadcast(msg);
479
- },
480
- });
611
+ yield open(url);
481
612
  }
482
- catch (err) {
483
- // plugin errors shouldn't crash server
484
- // eslint-disable-next-line no-console
485
- console.warn('plugin onHotUpdate error:', err.message);
613
+ catch (_b) {
614
+ // ignore open errors
486
615
  }
487
616
  }
488
- }
489
- // default: broadcast update for changed file
490
- broadcaster.broadcast({
491
- type: 'update',
492
- path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/'),
493
- });
494
- });
495
- // start server
496
- server.listen(port, async () => {
497
- const url = `http://localhost:${port}`;
498
- console.log(chalk_1.default.cyan.bold('\n🚀 React Client Dev Server'));
499
- console.log(chalk_1.default.green(`⚡ Running at: ${url}`));
500
- if (userConfig.server?.open !== false) {
501
- // open default browser
502
- try {
503
- await (0, open_1.default)(url);
504
- }
505
- catch {
506
- // ignore open errors
507
- }
508
- }
509
- });
510
- // graceful shutdown
511
- process.on('SIGINT', async () => {
512
- console.log(chalk_1.default.red('\n🛑 Shutting down...'));
513
- watcher.close();
514
- broadcaster.close();
515
- server.close();
516
- process.exit(0);
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
+ }));
517
626
  });
518
627
  }