react-client 1.0.28 → 1.0.31
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.
- package/dist/cli/commands/build.js +19 -25
- package/dist/cli/commands/dev.js +289 -205
- package/dist/cli/commands/init.js +34 -40
- package/dist/cli/commands/preview.js +41 -47
- package/dist/cli/index.js +49 -50
- package/dist/cli/types.js +1 -3
- package/dist/index.js +2 -20
- package/dist/server/broadcastManager.js +8 -15
- package/dist/utils/loadConfig.js +24 -63
- package/dist/utils/string.js +1 -4
- package/package.json +7 -6
package/dist/cli/commands/dev.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
|
-
* dev.ts —
|
|
2
|
+
* dev.ts — dev server for react-client
|
|
4
3
|
*
|
|
5
4
|
* - prebundles deps into .react-client/deps
|
|
6
5
|
* - serves /@modules/<dep>
|
|
@@ -10,45 +9,187 @@
|
|
|
10
9
|
*
|
|
11
10
|
* Keep this file linted & typed. Avoids manual react-dom/client hacks.
|
|
12
11
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
12
|
+
import esbuild from 'esbuild';
|
|
13
|
+
import connect from 'connect';
|
|
14
|
+
import http from 'http';
|
|
15
|
+
import chokidar from 'chokidar';
|
|
16
|
+
import detectPort from 'detect-port';
|
|
17
|
+
import prompts from 'prompts';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import fs from 'fs-extra';
|
|
20
|
+
import open from 'open';
|
|
21
|
+
import chalk from 'chalk';
|
|
22
|
+
import { execSync } from 'child_process';
|
|
23
|
+
import { loadReactClientConfig } from '../../utils/loadConfig';
|
|
24
|
+
import { BroadcastManager } from '../../server/broadcastManager';
|
|
25
|
+
import { createRequire } from 'module';
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const RUNTIME_OVERLAY_ROUTE = '/@runtime/overlay';
|
|
28
|
+
function jsContentType() {
|
|
29
|
+
return 'application/javascript; charset=utf-8';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve any bare import id robustly:
|
|
33
|
+
* 1. try require.resolve(id)
|
|
34
|
+
* 2. try require.resolve(`${pkg}/${subpath}`)
|
|
35
|
+
* 3. try package.json exports field
|
|
36
|
+
* 4. try common fallback candidates
|
|
37
|
+
*/
|
|
38
|
+
async function resolveModuleEntry(id, root) {
|
|
39
|
+
// quick resolution
|
|
40
|
+
try {
|
|
41
|
+
return require.resolve(id, { paths: [root] });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// continue
|
|
45
|
+
}
|
|
46
|
+
// split package root and subpath
|
|
47
|
+
const parts = id.split('/');
|
|
48
|
+
const pkgRoot = parts[0].startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
49
|
+
const subPath = parts.slice(pkgRoot.startsWith('@') ? 2 : 1).join('/');
|
|
50
|
+
let pkgJsonPath;
|
|
51
|
+
try {
|
|
52
|
+
pkgJsonPath = require.resolve(`${pkgRoot}/package.json`, { paths: [root] });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// No need to keep unused variable 'err'
|
|
56
|
+
throw new Error(`Package not found: ${pkgRoot}`);
|
|
57
|
+
}
|
|
58
|
+
const pkgDir = path.dirname(pkgJsonPath);
|
|
59
|
+
// Explicitly type pkgJson to avoid 'any'
|
|
60
|
+
let pkgJson = {};
|
|
61
|
+
try {
|
|
62
|
+
const pkgContent = await fs.readFile(pkgJsonPath, 'utf8');
|
|
63
|
+
pkgJson = JSON.parse(pkgContent);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// ignore parse or read errors gracefully
|
|
67
|
+
}
|
|
68
|
+
// If exports field exists, try to look up subpath (type-safe, supports conditional exports)
|
|
69
|
+
if (pkgJson.exports) {
|
|
70
|
+
const exportsField = pkgJson.exports;
|
|
71
|
+
// If exports is a plain string -> it's the entry
|
|
72
|
+
if (typeof exportsField === 'string') {
|
|
73
|
+
if (!subPath)
|
|
74
|
+
return path.resolve(pkgDir, exportsField);
|
|
75
|
+
}
|
|
76
|
+
else if (exportsField && typeof exportsField === 'object') {
|
|
77
|
+
// Normalize to a record so we can index it safely
|
|
78
|
+
const exportsMap = exportsField;
|
|
79
|
+
// Try candidates in order: explicit subpath, index, fallback
|
|
80
|
+
const keyCandidates = [];
|
|
81
|
+
if (subPath) {
|
|
82
|
+
keyCandidates.push(`./${subPath}`, `./${subPath}.js`, `./${subPath}.mjs`);
|
|
83
|
+
}
|
|
84
|
+
keyCandidates.push('.', './index.js', './index.mjs');
|
|
85
|
+
for (const key of keyCandidates) {
|
|
86
|
+
if (!(key in exportsMap))
|
|
87
|
+
continue;
|
|
88
|
+
const entry = exportsMap[key];
|
|
89
|
+
// entry may be string or object like { import: "...", require: "..." }
|
|
90
|
+
let target;
|
|
91
|
+
if (typeof entry === 'string') {
|
|
92
|
+
target = entry;
|
|
93
|
+
}
|
|
94
|
+
else if (entry && typeof entry === 'object') {
|
|
95
|
+
const entryObj = entry;
|
|
96
|
+
// Prefer "import" field for ESM consumers, then "default", then any string-ish value
|
|
97
|
+
if (typeof entryObj.import === 'string')
|
|
98
|
+
target = entryObj.import;
|
|
99
|
+
else if (typeof entryObj.default === 'string')
|
|
100
|
+
target = entryObj.default;
|
|
101
|
+
else {
|
|
102
|
+
// If the entry object itself is a conditional map (like {"node": "...", "browser": "..."}),
|
|
103
|
+
// attempt to pick any string value present.
|
|
104
|
+
for (const k of Object.keys(entryObj)) {
|
|
105
|
+
if (typeof entryObj[k] === 'string') {
|
|
106
|
+
target = entryObj[k];
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!target || typeof target !== 'string')
|
|
113
|
+
continue;
|
|
114
|
+
// Normalize relative paths in exports (remove leading ./)
|
|
115
|
+
const normalized = target.replace(/^\.\//, '');
|
|
116
|
+
const abs = path.isAbsolute(normalized)
|
|
117
|
+
? normalized
|
|
118
|
+
: path.resolve(pkgDir, normalized);
|
|
119
|
+
if (await fs.pathExists(abs)) {
|
|
120
|
+
return abs;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Try resolved subpath directly (pkg/subpath)
|
|
126
|
+
if (subPath) {
|
|
127
|
+
try {
|
|
128
|
+
const candidate = require.resolve(`${pkgRoot}/${subPath}`, { paths: [root] });
|
|
129
|
+
return candidate;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// fallback to searching common candidates under package dir
|
|
133
|
+
const candPaths = [
|
|
134
|
+
path.join(pkgDir, subPath),
|
|
135
|
+
path.join(pkgDir, subPath + '.js'),
|
|
136
|
+
path.join(pkgDir, subPath + '.mjs'),
|
|
137
|
+
path.join(pkgDir, subPath, 'index.js'),
|
|
138
|
+
path.join(pkgDir, subPath, 'index.mjs'),
|
|
139
|
+
];
|
|
140
|
+
for (const c of candPaths) {
|
|
141
|
+
if (await fs.pathExists(c))
|
|
142
|
+
return c;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Try package's main/module/browser fields safely (typed as string)
|
|
147
|
+
const candidateFields = [
|
|
148
|
+
typeof pkgJson.module === 'string' ? pkgJson.module : undefined,
|
|
149
|
+
typeof pkgJson.browser === 'string' ? pkgJson.browser : undefined,
|
|
150
|
+
typeof pkgJson.main === 'string' ? pkgJson.main : undefined,
|
|
151
|
+
];
|
|
152
|
+
for (const field of candidateFields) {
|
|
153
|
+
if (!field)
|
|
154
|
+
continue;
|
|
155
|
+
const abs = path.isAbsolute(field) ? field : path.resolve(pkgDir, field);
|
|
156
|
+
if (await fs.pathExists(abs))
|
|
157
|
+
return abs;
|
|
158
|
+
}
|
|
159
|
+
throw new Error(`Could not resolve module entry for ${id}`);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Wrap the built module for subpath imports:
|
|
163
|
+
* For requests like "/@modules/react-dom/client" — we bundle the resolved file
|
|
164
|
+
* and return it. If the user requested the package root instead, the resolved
|
|
165
|
+
* bundle is returned directly.
|
|
166
|
+
*
|
|
167
|
+
* No hardcoded special cases.
|
|
168
|
+
*/
|
|
169
|
+
function normalizeCacheKey(id) {
|
|
170
|
+
return id.replace(/[\\/]/g, '_');
|
|
171
|
+
}
|
|
172
|
+
export default async function dev() {
|
|
32
173
|
const root = process.cwd();
|
|
33
|
-
const userConfig = (await
|
|
34
|
-
const appRoot =
|
|
174
|
+
const userConfig = (await loadReactClientConfig(root));
|
|
175
|
+
const appRoot = path.resolve(root, userConfig.root || '.');
|
|
35
176
|
const defaultPort = userConfig.server?.port ?? 2202;
|
|
36
177
|
// cache dir for prebundled deps
|
|
37
|
-
const cacheDir =
|
|
38
|
-
await
|
|
178
|
+
const cacheDir = path.join(appRoot, '.react-client', 'deps');
|
|
179
|
+
await fs.ensureDir(cacheDir);
|
|
39
180
|
// Detect entry (main.tsx / main.jsx)
|
|
40
|
-
const possible = ['src/main.tsx', 'src/main.jsx'].map((p) =>
|
|
41
|
-
const entry = possible.find((p) =>
|
|
181
|
+
const possible = ['src/main.tsx', 'src/main.jsx'].map((p) => path.join(appRoot, p));
|
|
182
|
+
const entry = possible.find((p) => fs.existsSync(p));
|
|
42
183
|
if (!entry) {
|
|
43
|
-
console.error(
|
|
184
|
+
console.error(chalk.red('❌ Entry not found: src/main.tsx or src/main.jsx'));
|
|
44
185
|
process.exit(1);
|
|
45
186
|
}
|
|
46
|
-
const indexHtml =
|
|
187
|
+
const indexHtml = path.join(appRoot, 'index.html');
|
|
47
188
|
// Select port
|
|
48
|
-
const availablePort = await (
|
|
189
|
+
const availablePort = await detectPort(defaultPort);
|
|
49
190
|
const port = availablePort;
|
|
50
191
|
if (availablePort !== defaultPort) {
|
|
51
|
-
const response = await (
|
|
192
|
+
const response = await prompts({
|
|
52
193
|
type: 'confirm',
|
|
53
194
|
name: 'useNewPort',
|
|
54
195
|
message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
|
|
@@ -64,11 +205,16 @@ async function dev() {
|
|
|
64
205
|
require.resolve('react-refresh/runtime');
|
|
65
206
|
}
|
|
66
207
|
catch {
|
|
67
|
-
console.warn(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
208
|
+
console.warn(chalk.yellow('⚠️ react-refresh not found — installing react-refresh...'));
|
|
209
|
+
try {
|
|
210
|
+
execSync('npm install react-refresh --no-audit --no-fund --silent', {
|
|
211
|
+
cwd: appRoot,
|
|
212
|
+
stdio: 'inherit',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
console.warn(chalk.yellow('⚠️ automatic install of react-refresh failed; continuing without it.'));
|
|
217
|
+
}
|
|
72
218
|
}
|
|
73
219
|
// Plugin system (core + user)
|
|
74
220
|
const corePlugins = [
|
|
@@ -92,7 +238,7 @@ async function dev() {
|
|
|
92
238
|
const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
|
|
93
239
|
const plugins = [...corePlugins, ...userPlugins];
|
|
94
240
|
// App + caches
|
|
95
|
-
const app = (
|
|
241
|
+
const app = connect();
|
|
96
242
|
const transformCache = new Map();
|
|
97
243
|
// Helper: recursively analyze dependency graph for prebundling (bare imports)
|
|
98
244
|
async function analyzeGraph(file, seen = new Set()) {
|
|
@@ -100,7 +246,7 @@ async function dev() {
|
|
|
100
246
|
return seen;
|
|
101
247
|
seen.add(file);
|
|
102
248
|
try {
|
|
103
|
-
const code = await
|
|
249
|
+
const code = await fs.readFile(file, 'utf8');
|
|
104
250
|
const matches = [
|
|
105
251
|
...code.matchAll(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g),
|
|
106
252
|
...code.matchAll(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g),
|
|
@@ -128,17 +274,17 @@ async function dev() {
|
|
|
128
274
|
async function prebundleDeps(deps) {
|
|
129
275
|
if (!deps.size)
|
|
130
276
|
return;
|
|
131
|
-
const existingFiles = await
|
|
277
|
+
const existingFiles = await fs.readdir(cacheDir);
|
|
132
278
|
const existing = new Set(existingFiles.map((f) => f.replace(/\.js$/, '')));
|
|
133
279
|
const missing = [...deps].filter((d) => !existing.has(d));
|
|
134
280
|
if (!missing.length)
|
|
135
281
|
return;
|
|
136
|
-
console.log(
|
|
282
|
+
console.log(chalk.cyan('📦 Prebundling:'), missing.join(', '));
|
|
137
283
|
await Promise.all(missing.map(async (dep) => {
|
|
138
284
|
try {
|
|
139
285
|
const entryPoint = require.resolve(dep, { paths: [appRoot] });
|
|
140
|
-
const outFile =
|
|
141
|
-
await
|
|
286
|
+
const outFile = path.join(cacheDir, normalizeCacheKey(dep) + '.js');
|
|
287
|
+
await esbuild.build({
|
|
142
288
|
entryPoints: [entryPoint],
|
|
143
289
|
bundle: true,
|
|
144
290
|
platform: 'browser',
|
|
@@ -147,10 +293,10 @@ async function dev() {
|
|
|
147
293
|
write: true,
|
|
148
294
|
target: ['es2020'],
|
|
149
295
|
});
|
|
150
|
-
console.log(
|
|
296
|
+
console.log(chalk.green(`✅ Cached ${dep}`));
|
|
151
297
|
}
|
|
152
298
|
catch (err) {
|
|
153
|
-
console.warn(
|
|
299
|
+
console.warn(chalk.yellow(`⚠️ Skipped ${dep}: ${err.message}`));
|
|
154
300
|
}
|
|
155
301
|
}));
|
|
156
302
|
}
|
|
@@ -158,16 +304,16 @@ async function dev() {
|
|
|
158
304
|
const depsSet = await analyzeGraph(entry);
|
|
159
305
|
await prebundleDeps(depsSet);
|
|
160
306
|
// Watch package.json for changes to re-prebundle
|
|
161
|
-
const pkgPath =
|
|
162
|
-
if (await
|
|
163
|
-
|
|
164
|
-
console.log(
|
|
307
|
+
const pkgPath = path.join(appRoot, 'package.json');
|
|
308
|
+
if (await fs.pathExists(pkgPath)) {
|
|
309
|
+
chokidar.watch(pkgPath).on('change', async () => {
|
|
310
|
+
console.log(chalk.yellow('📦 package.json changed — rebuilding prebundle...'));
|
|
165
311
|
const newDeps = await analyzeGraph(entry);
|
|
166
312
|
await prebundleDeps(newDeps);
|
|
167
313
|
});
|
|
168
314
|
}
|
|
169
315
|
// --- Serve /@modules/<dep> (prebundled or on-demand esbuild bundle)
|
|
170
|
-
app.use(async (req, res, next) => {
|
|
316
|
+
app.use((async (req, res, next) => {
|
|
171
317
|
const url = req.url ?? '';
|
|
172
318
|
if (!url.startsWith('/@modules/'))
|
|
173
319
|
return next();
|
|
@@ -177,44 +323,14 @@ async function dev() {
|
|
|
177
323
|
return res.end('// invalid module');
|
|
178
324
|
}
|
|
179
325
|
try {
|
|
180
|
-
const cacheFile =
|
|
181
|
-
if (await
|
|
182
|
-
res.setHeader('Content-Type',
|
|
183
|
-
return res.end(await
|
|
184
|
-
}
|
|
185
|
-
// 🧠 Handle subpath imports correctly (like react-dom/client)
|
|
186
|
-
let entryFile = null;
|
|
187
|
-
try {
|
|
188
|
-
entryFile = require.resolve(id, { paths: [appRoot] });
|
|
326
|
+
const cacheFile = path.join(cacheDir, normalizeCacheKey(id) + '.js');
|
|
327
|
+
if (await fs.pathExists(cacheFile)) {
|
|
328
|
+
res.setHeader('Content-Type', jsContentType());
|
|
329
|
+
return res.end(await fs.readFile(cacheFile, 'utf8'));
|
|
189
330
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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({
|
|
331
|
+
// Resolve the actual entry file (handles subpaths & package exports)
|
|
332
|
+
const entryFile = await resolveModuleEntry(id, appRoot);
|
|
333
|
+
const result = await esbuild.build({
|
|
218
334
|
entryPoints: [entryFile],
|
|
219
335
|
bundle: true,
|
|
220
336
|
platform: 'browser',
|
|
@@ -223,108 +339,87 @@ async function dev() {
|
|
|
223
339
|
target: ['es2020'],
|
|
224
340
|
});
|
|
225
341
|
const output = result.outputFiles?.[0]?.text ?? '';
|
|
226
|
-
|
|
227
|
-
|
|
342
|
+
// Write cache and respond
|
|
343
|
+
await fs.writeFile(cacheFile, output, 'utf8');
|
|
344
|
+
res.setHeader('Content-Type', jsContentType());
|
|
228
345
|
res.end(output);
|
|
229
346
|
}
|
|
230
347
|
catch (err) {
|
|
231
348
|
res.writeHead(500);
|
|
232
349
|
res.end(`// Failed to resolve module ${id}: ${err.message}`);
|
|
233
350
|
}
|
|
234
|
-
});
|
|
351
|
+
}));
|
|
235
352
|
// --- Serve runtime overlay (inline, no external dependencies)
|
|
236
353
|
const OVERLAY_RUNTIME = `
|
|
354
|
+
/* inline overlay runtime - served at ${RUNTIME_OVERLAY_ROUTE} */
|
|
355
|
+
${(() => {
|
|
356
|
+
// small helper — embed as a string
|
|
357
|
+
return `
|
|
237
358
|
const overlayId = "__rc_error_overlay__";
|
|
359
|
+
(function(){
|
|
360
|
+
const style = document.createElement("style");
|
|
361
|
+
style.textContent = \`
|
|
362
|
+
#\${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;}
|
|
363
|
+
#\${overlayId} h2{color:#ff6b6b;margin-bottom:16px;}
|
|
364
|
+
#\${overlayId} pre{background:rgba(255,255,255,0.06);padding:12px;border-radius:6px;overflow:auto;}
|
|
365
|
+
.frame-file{color:#ffa500;cursor:pointer;font-weight:bold;margin-bottom:4px;}
|
|
366
|
+
.line-number{opacity:0.6;margin-right:10px;display:inline-block;width:2em;text-align:right;}
|
|
367
|
+
\`;
|
|
368
|
+
document.head.appendChild(style);
|
|
238
369
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
padding: 24px;
|
|
251
|
-
animation: fadeIn 0.2s ease-out;
|
|
370
|
+
async function mapStackFrame(frame){
|
|
371
|
+
const m = frame.match(/(\\/src\\/[^\s:]+):(\\d+):(\\d+)/);
|
|
372
|
+
if(!m) return frame;
|
|
373
|
+
const [,file,line,col] = m;
|
|
374
|
+
try{
|
|
375
|
+
const resp = await fetch(\`/@source-map?file=\${file}&line=\${line}&column=\${col}\`);
|
|
376
|
+
if(!resp.ok) return frame;
|
|
377
|
+
const pos = await resp.json();
|
|
378
|
+
if(pos.source) return pos;
|
|
379
|
+
}catch(e){}
|
|
380
|
+
return frame;
|
|
252
381
|
}
|
|
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
382
|
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
};
|
|
383
|
+
function highlightSimple(s){
|
|
384
|
+
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
385
|
}
|
|
278
|
-
return frame;
|
|
279
|
-
}
|
|
280
386
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
.
|
|
286
|
-
.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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);
|
|
387
|
+
async function renderOverlay(err){
|
|
388
|
+
const overlay = document.getElementById(overlayId) || document.body.appendChild(Object.assign(document.createElement("div"),{id:overlayId}));
|
|
389
|
+
overlay.innerHTML = "";
|
|
390
|
+
const title = document.createElement("h2");
|
|
391
|
+
title.textContent = "🔥 " + (err.message || "Error");
|
|
392
|
+
overlay.appendChild(title);
|
|
393
|
+
const frames = (err.stack||"").split("\\n").filter(l => /src\\//.test(l));
|
|
394
|
+
for(const frame of frames){
|
|
395
|
+
const mapped = await mapStackFrame(frame);
|
|
396
|
+
if(typeof mapped === "string") continue;
|
|
397
|
+
const frameEl = document.createElement("div");
|
|
398
|
+
const link = document.createElement("div");
|
|
399
|
+
link.className = "frame-file";
|
|
400
|
+
link.textContent = \`\${mapped.source||mapped.file}:\${mapped.line}:\${mapped.column}\`;
|
|
401
|
+
link.onclick = ()=>window.open("vscode://file/"+(mapped.source||mapped.file)+":"+mapped.line);
|
|
402
|
+
frameEl.appendChild(link);
|
|
403
|
+
if(mapped.snippet){
|
|
404
|
+
const pre = document.createElement("pre");
|
|
405
|
+
pre.innerHTML = highlightSimple(mapped.snippet);
|
|
406
|
+
frameEl.appendChild(pre);
|
|
407
|
+
}
|
|
408
|
+
overlay.appendChild(frameEl);
|
|
316
409
|
}
|
|
317
|
-
|
|
318
|
-
overlay.appendChild(frameEl);
|
|
319
410
|
}
|
|
320
|
-
}
|
|
321
411
|
|
|
322
|
-
window.showErrorOverlay = (err)
|
|
323
|
-
window.clearErrorOverlay = ()
|
|
412
|
+
window.showErrorOverlay = (err)=>renderOverlay(err);
|
|
413
|
+
window.clearErrorOverlay = ()=>document.getElementById(overlayId)?.remove();
|
|
414
|
+
window.addEventListener("error", e => window.showErrorOverlay?.(e.error || e));
|
|
415
|
+
window.addEventListener("unhandledrejection", e => window.showErrorOverlay?.(e.reason || e));
|
|
416
|
+
})();
|
|
417
|
+
`;
|
|
418
|
+
})()}
|
|
324
419
|
`;
|
|
325
420
|
app.use(async (req, res, next) => {
|
|
326
|
-
if (req.url ===
|
|
327
|
-
res.setHeader('Content-Type',
|
|
421
|
+
if (req.url === RUNTIME_OVERLAY_ROUTE) {
|
|
422
|
+
res.setHeader('Content-Type', jsContentType());
|
|
328
423
|
return res.end(OVERLAY_RUNTIME);
|
|
329
424
|
}
|
|
330
425
|
next();
|
|
@@ -334,10 +429,8 @@ window.clearErrorOverlay = () => document.getElementById(overlayId)?.remove();
|
|
|
334
429
|
const url = req.url ?? '';
|
|
335
430
|
if (!url.startsWith('/@source-map'))
|
|
336
431
|
return next();
|
|
337
|
-
// expected query: ?file=/src/xyz.tsx&line=12&column=3
|
|
338
432
|
try {
|
|
339
|
-
const
|
|
340
|
-
const parsed = new URL(full, `http://localhost:${port}`);
|
|
433
|
+
const parsed = new URL(req.url ?? '', `http://localhost:${port}`);
|
|
341
434
|
const file = parsed.searchParams.get('file') ?? '';
|
|
342
435
|
const lineStr = parsed.searchParams.get('line') ?? '0';
|
|
343
436
|
const lineNum = Number(lineStr) || 0;
|
|
@@ -345,12 +438,12 @@ window.clearErrorOverlay = () => document.getElementById(overlayId)?.remove();
|
|
|
345
438
|
res.writeHead(400);
|
|
346
439
|
return res.end('{}');
|
|
347
440
|
}
|
|
348
|
-
const filePath =
|
|
349
|
-
if (!(await
|
|
441
|
+
const filePath = path.join(appRoot, file.startsWith('/') ? file.slice(1) : file);
|
|
442
|
+
if (!(await fs.pathExists(filePath))) {
|
|
350
443
|
res.writeHead(404);
|
|
351
444
|
return res.end('{}');
|
|
352
445
|
}
|
|
353
|
-
const src = await
|
|
446
|
+
const src = await fs.readFile(filePath, 'utf8');
|
|
354
447
|
const lines = src.split(/\r?\n/);
|
|
355
448
|
const start = Math.max(0, lineNum - 3 - 1);
|
|
356
449
|
const end = Math.min(lines.length, lineNum + 2);
|
|
@@ -377,12 +470,12 @@ window.clearErrorOverlay = () => document.getElementById(overlayId)?.remove();
|
|
|
377
470
|
if (!url.startsWith('/src/') && !url.endsWith('.css'))
|
|
378
471
|
return next();
|
|
379
472
|
const raw = decodeURIComponent((req.url ?? '').split('?')[0]);
|
|
380
|
-
const filePath =
|
|
473
|
+
const filePath = path.join(appRoot, raw.replace(/^\//, ''));
|
|
381
474
|
// Try file extensions if not exact file
|
|
382
475
|
const exts = ['', '.tsx', '.ts', '.jsx', '.js', '.css'];
|
|
383
476
|
let found = '';
|
|
384
477
|
for (const ext of exts) {
|
|
385
|
-
if (await
|
|
478
|
+
if (await fs.pathExists(filePath + ext)) {
|
|
386
479
|
found = filePath + ext;
|
|
387
480
|
break;
|
|
388
481
|
}
|
|
@@ -390,7 +483,7 @@ window.clearErrorOverlay = () => document.getElementById(overlayId)?.remove();
|
|
|
390
483
|
if (!found)
|
|
391
484
|
return next();
|
|
392
485
|
try {
|
|
393
|
-
let code = await
|
|
486
|
+
let code = await fs.readFile(found, 'utf8');
|
|
394
487
|
// rewrite bare imports -> /@modules/<dep>
|
|
395
488
|
code = code
|
|
396
489
|
.replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
|
|
@@ -398,24 +491,21 @@ window.clearErrorOverlay = () => document.getElementById(overlayId)?.remove();
|
|
|
398
491
|
// run plugin transforms
|
|
399
492
|
for (const p of plugins) {
|
|
400
493
|
if (p.onTransform) {
|
|
401
|
-
// plugin may return transformed code
|
|
402
|
-
// keep typed as string
|
|
403
|
-
// eslint-disable-next-line no-await-in-loop
|
|
404
494
|
const out = await p.onTransform(code, found);
|
|
405
495
|
if (typeof out === 'string')
|
|
406
496
|
code = out;
|
|
407
497
|
}
|
|
408
498
|
}
|
|
409
499
|
// choose loader by extension
|
|
410
|
-
const ext =
|
|
500
|
+
const ext = path.extname(found).toLowerCase();
|
|
411
501
|
const loader = ext === '.ts' ? 'ts' : ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'js';
|
|
412
|
-
const result = await
|
|
502
|
+
const result = await esbuild.transform(code, {
|
|
413
503
|
loader,
|
|
414
504
|
sourcemap: 'inline',
|
|
415
505
|
target: ['es2020'],
|
|
416
506
|
});
|
|
417
507
|
transformCache.set(found, result.code);
|
|
418
|
-
res.setHeader('Content-Type',
|
|
508
|
+
res.setHeader('Content-Type', jsContentType());
|
|
419
509
|
res.end(result.code);
|
|
420
510
|
}
|
|
421
511
|
catch (err) {
|
|
@@ -429,15 +519,15 @@ window.clearErrorOverlay = () => document.getElementById(overlayId)?.remove();
|
|
|
429
519
|
const url = req.url ?? '';
|
|
430
520
|
if (url !== '/' && url !== '/index.html')
|
|
431
521
|
return next();
|
|
432
|
-
if (!(await
|
|
522
|
+
if (!(await fs.pathExists(indexHtml))) {
|
|
433
523
|
res.writeHead(404);
|
|
434
524
|
return res.end('index.html not found');
|
|
435
525
|
}
|
|
436
526
|
try {
|
|
437
|
-
let html = await
|
|
527
|
+
let html = await fs.readFile(indexHtml, 'utf8');
|
|
438
528
|
// inject overlay runtime and HMR client if not already present
|
|
439
|
-
if (!html.includes(
|
|
440
|
-
html = html.replace('</body>', `\n<script type="module" src="
|
|
529
|
+
if (!html.includes(RUNTIME_OVERLAY_ROUTE)) {
|
|
530
|
+
html = html.replace('</body>', `\n<script type="module" src="${RUNTIME_OVERLAY_ROUTE}"></script>\n<script type="module">
|
|
441
531
|
const ws = new WebSocket("ws://" + location.host);
|
|
442
532
|
ws.onmessage = (e) => {
|
|
443
533
|
const msg = JSON.parse(e.data);
|
|
@@ -450,7 +540,7 @@ window.clearErrorOverlay = () => document.getElementById(overlayId)?.remove();
|
|
|
450
540
|
};
|
|
451
541
|
</script>\n</body>`);
|
|
452
542
|
}
|
|
453
|
-
res.setHeader('Content-Type', 'text/html');
|
|
543
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
454
544
|
res.end(html);
|
|
455
545
|
}
|
|
456
546
|
catch (err) {
|
|
@@ -459,29 +549,24 @@ window.clearErrorOverlay = () => document.getElementById(overlayId)?.remove();
|
|
|
459
549
|
}
|
|
460
550
|
}));
|
|
461
551
|
// --- HMR WebSocket server
|
|
462
|
-
const server =
|
|
463
|
-
const broadcaster = new
|
|
552
|
+
const server = http.createServer(app);
|
|
553
|
+
const broadcaster = new BroadcastManager(server);
|
|
464
554
|
// Watch files and trigger plugin onHotUpdate + broadcast HMR message
|
|
465
|
-
const watcher =
|
|
555
|
+
const watcher = chokidar.watch(path.join(appRoot, 'src'), { ignoreInitial: true });
|
|
466
556
|
watcher.on('change', async (file) => {
|
|
467
557
|
transformCache.delete(file);
|
|
468
558
|
// plugin hook onHotUpdate optionally
|
|
469
559
|
for (const p of plugins) {
|
|
470
560
|
if (p.onHotUpdate) {
|
|
471
561
|
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
562
|
await p.onHotUpdate(file, {
|
|
563
|
+
// plugin only needs broadcast in most cases
|
|
477
564
|
broadcast: (msg) => {
|
|
478
565
|
broadcaster.broadcast(msg);
|
|
479
566
|
},
|
|
480
567
|
});
|
|
481
568
|
}
|
|
482
569
|
catch (err) {
|
|
483
|
-
// plugin errors shouldn't crash server
|
|
484
|
-
// eslint-disable-next-line no-console
|
|
485
570
|
console.warn('plugin onHotUpdate error:', err.message);
|
|
486
571
|
}
|
|
487
572
|
}
|
|
@@ -489,18 +574,17 @@ window.clearErrorOverlay = () => document.getElementById(overlayId)?.remove();
|
|
|
489
574
|
// default: broadcast update for changed file
|
|
490
575
|
broadcaster.broadcast({
|
|
491
576
|
type: 'update',
|
|
492
|
-
path: '/' +
|
|
577
|
+
path: '/' + path.relative(appRoot, file).replace(/\\/g, '/'),
|
|
493
578
|
});
|
|
494
579
|
});
|
|
495
580
|
// start server
|
|
496
581
|
server.listen(port, async () => {
|
|
497
582
|
const url = `http://localhost:${port}`;
|
|
498
|
-
console.log(
|
|
499
|
-
console.log(
|
|
583
|
+
console.log(chalk.cyan.bold('\n🚀 React Client Dev Server'));
|
|
584
|
+
console.log(chalk.green(`⚡ Running at: ${url}`));
|
|
500
585
|
if (userConfig.server?.open !== false) {
|
|
501
|
-
// open default browser
|
|
502
586
|
try {
|
|
503
|
-
await (
|
|
587
|
+
await open(url);
|
|
504
588
|
}
|
|
505
589
|
catch {
|
|
506
590
|
// ignore open errors
|
|
@@ -509,8 +593,8 @@ window.clearErrorOverlay = () => document.getElementById(overlayId)?.remove();
|
|
|
509
593
|
});
|
|
510
594
|
// graceful shutdown
|
|
511
595
|
process.on('SIGINT', async () => {
|
|
512
|
-
console.log(
|
|
513
|
-
watcher.close();
|
|
596
|
+
console.log(chalk.red('\n🛑 Shutting down...'));
|
|
597
|
+
await watcher.close();
|
|
514
598
|
broadcaster.close();
|
|
515
599
|
server.close();
|
|
516
600
|
process.exit(0);
|