react-client 1.0.27 → 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,439 +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] });
368
+ res.writeHead(500);
369
+ res.end(`// Failed to resolve module ${id}: ${err.message}`);
189
370
  }
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
- }
214
- }
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
- app.use(async (req, res, next) => {
236
- if (req.url?.startsWith('/@prismjs')) {
237
- const prismPath = require.resolve('prismjs', { paths: [appRoot] });
238
- const code = await fs_extra_1.default.readFile(prismPath, 'utf8');
239
- res.setHeader('Content-Type', 'application/javascript');
240
- return res.end(code);
241
- }
242
- next();
243
- });
244
- // --- Serve runtime overlay (local file) so overlay-runtime.js is loaded automatically
245
- // --- Serve runtime overlay (inline in dev server)
246
- const OVERLAY_RUNTIME = `
247
- import "/@prismjs";
248
-
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 `
249
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);
250
389
 
251
- const style = document.createElement("style");
252
- style.textContent = \`
253
- #\${overlayId} {
254
- position: fixed;
255
- inset: 0;
256
- background: rgba(0, 0, 0, 0.9);
257
- color: #fff;
258
- font-family: Menlo, Consolas, monospace;
259
- font-size: 14px;
260
- z-index: 999999;
261
- overflow: auto;
262
- padding: 24px;
263
- 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;
264
401
  }
265
- @keyframes fadeIn { from {opacity: 0;} to {opacity: 1;} }
266
- #\${overlayId} h2 { color: #ff6b6b; margin-bottom: 16px; }
267
- #\${overlayId} pre { background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; }
268
- #\${overlayId} a { color: #9cf; text-decoration: underline; }
269
- #\${overlayId} .frame { margin: 12px 0; }
270
- #\${overlayId} .frame-file { color: #ffa500; cursor: pointer; font-weight: bold; margin-bottom: 4px; }
271
- .line-number { opacity: 0.5; margin-right: 10px; }
272
- \`;
273
- document.head.appendChild(style);
274
402
 
275
- async function mapStackFrame(frame) {
276
- const m = frame.match(/(\\/src\\/[^\s:]+):(\\d+):(\\d+)/);
277
- if (!m) return frame;
278
- const [, file, line, col] = m;
279
- const resp = await fetch(\`/@source-map?file=\${file}&line=\${line}&column=\${col}\`);
280
- if (!resp.ok) return frame;
281
- const pos = await resp.json();
282
- if (pos.source) {
283
- return {
284
- file: pos.source,
285
- line: pos.line,
286
- column: pos.column,
287
- snippet: pos.snippet || ""
288
- };
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>');
289
405
  }
290
- return frame;
291
- }
292
-
293
- async function renderOverlay(err) {
294
- const overlay =
295
- document.getElementById(overlayId) ||
296
- document.body.appendChild(Object.assign(document.createElement("div"), { id: overlayId }));
297
- overlay.innerHTML = "";
298
- const title = document.createElement("h2");
299
- title.textContent = "🔥 " + (err.message || "Error");
300
- overlay.appendChild(title);
301
406
 
302
- const frames = (err.stack || "").split("\\n").filter(l => /src\\//.test(l));
303
- for (const frame of frames) {
304
- const mapped = await mapStackFrame(frame);
305
- if (typeof mapped === "string") continue;
306
- const frameEl = document.createElement("div");
307
- frameEl.className = "frame";
308
-
309
- const link = document.createElement("div");
310
- link.className = "frame-file";
311
- link.textContent = \`\${mapped.file}:\${mapped.line}:\${mapped.column}\`;
312
- link.onclick = () =>
313
- window.open("vscode://file/" + location.origin.replace("http://", "") + mapped.file + ":" + mapped.line);
314
- frameEl.appendChild(link);
315
-
316
- if (mapped.snippet) {
317
- const pre = document.createElement("pre");
318
- pre.classList.add("language-jsx");
319
- pre.innerHTML = Prism.highlight(mapped.snippet, Prism.languages.jsx, "jsx");
320
- 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);
321
429
  }
322
-
323
- overlay.appendChild(frameEl);
324
430
  }
325
- }
326
431
 
327
- window.showErrorOverlay = (err) => renderOverlay(err);
328
- 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
+ })();
329
437
  `;
330
- app.use(async (req, res, next) => {
331
- if (req.url === '/@runtime/overlay') {
332
- res.setHeader('Content-Type', 'application/javascript');
333
- return res.end(OVERLAY_RUNTIME);
334
- }
335
- next();
336
- });
337
- // --- minimal /@source-map: return snippet around requested line of original source file
338
- app.use((async (req, res, next) => {
339
- const url = req.url ?? '';
340
- if (!url.startsWith('/@source-map'))
341
- return next();
342
- // expected query: ?file=/src/xyz.tsx&line=12&column=3
343
- try {
344
- const full = req.url ?? '';
345
- const parsed = new URL(full, `http://localhost:${port}`);
346
- const file = parsed.searchParams.get('file') ?? '';
347
- const lineStr = parsed.searchParams.get('line') ?? '0';
348
- const lineNum = Number(lineStr) || 0;
349
- if (!file) {
350
- res.writeHead(400);
351
- 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);
352
444
  }
353
- const filePath = path_1.default.join(appRoot, file.startsWith('/') ? file.slice(1) : file);
354
- if (!(await fs_extra_1.default.pathExists(filePath))) {
355
- res.writeHead(404);
356
- 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 }));
357
482
  }
358
- const src = await fs_extra_1.default.readFile(filePath, 'utf8');
359
- const lines = src.split(/\r?\n/);
360
- const start = Math.max(0, lineNum - 3 - 1);
361
- const end = Math.min(lines.length, lineNum + 2);
362
- const snippet = lines
363
- .slice(start, end)
364
- .map((l, i) => {
365
- const ln = start + i + 1;
366
- return `<span class="line-number">${ln}</span> ${l
367
- .replace(/</g, '&lt;')
368
- .replace(/>/g, '&gt;')}`;
369
- })
370
- .join('\n');
371
- res.setHeader('Content-Type', 'application/json');
372
- res.end(JSON.stringify({ source: file, line: lineNum, column: 0, snippet }));
373
- }
374
- catch (err) {
375
- res.writeHead(500);
376
- res.end(JSON.stringify({ error: err.message }));
377
- }
378
- }));
379
- // --- Serve /src/* files (on-the-fly transform + bare import rewrite)
380
- app.use((async (req, res, next) => {
381
- const url = req.url ?? '';
382
- if (!url.startsWith('/src/') && !url.endsWith('.css'))
383
- return next();
384
- const raw = decodeURIComponent((req.url ?? '').split('?')[0]);
385
- const filePath = path_1.default.join(appRoot, raw.replace(/^\//, ''));
386
- // Try file extensions if not exact file
387
- const exts = ['', '.tsx', '.ts', '.jsx', '.js', '.css'];
388
- let found = '';
389
- for (const ext of exts) {
390
- if (await fs_extra_1.default.pathExists(filePath + ext)) {
391
- found = filePath + ext;
392
- break;
483
+ catch (err) {
484
+ res.writeHead(500);
485
+ res.end(JSON.stringify({ error: err.message }));
393
486
  }
394
- }
395
- if (!found)
396
- return next();
397
- try {
398
- let code = await fs_extra_1.default.readFile(found, 'utf8');
399
- // rewrite bare imports -> /@modules/<dep>
400
- code = code
401
- .replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
402
- .replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
403
- // run plugin transforms
404
- for (const p of plugins) {
405
- if (p.onTransform) {
406
- // plugin may return transformed code
407
- // keep typed as string
408
- // eslint-disable-next-line no-await-in-loop
409
- const out = await p.onTransform(code, found);
410
- if (typeof out === 'string')
411
- 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;
412
503
  }
413
504
  }
414
- // choose loader by extension
415
- const ext = path_1.default.extname(found).toLowerCase();
416
- const loader = ext === '.ts' ? 'ts' : ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'js';
417
- const result = await esbuild_1.default.transform(code, {
418
- loader,
419
- sourcemap: 'inline',
420
- target: ['es2020'],
421
- });
422
- transformCache.set(found, result.code);
423
- res.setHeader('Content-Type', 'application/javascript');
424
- res.end(result.code);
425
- }
426
- catch (err) {
427
- const e = err;
428
- res.writeHead(500);
429
- res.end(`// transform error: ${e.message}`);
430
- }
431
- }));
432
- // --- Serve index.html with overlay + HMR client injection
433
- app.use((async (req, res, next) => {
434
- const url = req.url ?? '';
435
- if (url !== '/' && url !== '/index.html')
436
- return next();
437
- if (!(await fs_extra_1.default.pathExists(indexHtml))) {
438
- res.writeHead(404);
439
- return res.end('index.html not found');
440
- }
441
- try {
442
- let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
443
- // inject overlay runtime and HMR client if not already present
444
- if (!html.includes('/@runtime/overlay')) {
445
- 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">
446
554
  const ws = new WebSocket("ws://" + location.host);
447
555
  ws.onmessage = (e) => {
448
556
  const msg = JSON.parse(e.data);
@@ -454,70 +562,66 @@ window.clearErrorOverlay = () => document.getElementById(overlayId)?.remove();
454
562
  }
455
563
  };
456
564
  </script>\n</body>`);
565
+ }
566
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
567
+ res.end(html);
457
568
  }
458
- res.setHeader('Content-Type', 'text/html');
459
- res.end(html);
460
- }
461
- catch (err) {
462
- res.writeHead(500);
463
- res.end(`// html read error: ${err.message}`);
464
- }
465
- }));
466
- // --- HMR WebSocket server
467
- const server = http_1.default.createServer(app);
468
- const broadcaster = new broadcastManager_1.BroadcastManager(server);
469
- // Watch files and trigger plugin onHotUpdate + broadcast HMR message
470
- const watcher = chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true });
471
- watcher.on('change', async (file) => {
472
- transformCache.delete(file);
473
- // plugin hook onHotUpdate optionally
474
- for (const p of plugins) {
475
- 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) {
476
610
  try {
477
- // allow plugin to broadcast via a simple function
478
- // plugin gets { broadcast }
479
- // plugin signature: onHotUpdate(file, { broadcast })
480
- // eslint-disable-next-line no-await-in-loop
481
- await p.onHotUpdate(file, {
482
- broadcast: (msg) => {
483
- broadcaster.broadcast(msg);
484
- },
485
- });
611
+ yield open(url);
486
612
  }
487
- catch (err) {
488
- // plugin errors shouldn't crash server
489
- // eslint-disable-next-line no-console
490
- console.warn('plugin onHotUpdate error:', err.message);
613
+ catch (_b) {
614
+ // ignore open errors
491
615
  }
492
616
  }
493
- }
494
- // default: broadcast update for changed file
495
- broadcaster.broadcast({
496
- type: 'update',
497
- path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/'),
498
- });
499
- });
500
- // start server
501
- server.listen(port, async () => {
502
- const url = `http://localhost:${port}`;
503
- console.log(chalk_1.default.cyan.bold('\n🚀 React Client Dev Server'));
504
- console.log(chalk_1.default.green(`⚡ Running at: ${url}`));
505
- if (userConfig.server?.open !== false) {
506
- // open default browser
507
- try {
508
- await (0, open_1.default)(url);
509
- }
510
- catch {
511
- // ignore open errors
512
- }
513
- }
514
- });
515
- // graceful shutdown
516
- process.on('SIGINT', async () => {
517
- console.log(chalk_1.default.red('\n🛑 Shutting down...'));
518
- watcher.close();
519
- broadcaster.close();
520
- server.close();
521
- 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
+ }));
522
626
  });
523
627
  }