react-client 1.0.12 → 1.0.14
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/README.md +3 -3
- package/dist/cli/commands/dev.js +119 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ npm install
|
|
|
35
35
|
npm run dev
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
This launches the **custom dev server** —
|
|
38
|
+
This launches the **custom dev server** — built on **Connect + WebSocket + esbuild**, featuring:
|
|
39
39
|
- Instant rebuilds
|
|
40
40
|
- React Fast Refresh (HMR)
|
|
41
41
|
- Auto port detection & confirmation prompt
|
|
@@ -85,9 +85,9 @@ Each template is pre-configured for esbuild, HMR, and fast bootstrapping.
|
|
|
85
85
|
|
|
86
86
|
## 💎 Core Features
|
|
87
87
|
|
|
88
|
-
- ⚡ **Custom Dev Server
|
|
88
|
+
- ⚡ **Custom Dev Server** — Connect + WebSocket + esbuild
|
|
89
89
|
- 🔁 **React Fast Refresh (HMR)** — State-preserving reloads
|
|
90
|
-
- 💥 **
|
|
90
|
+
- 💥 **Overlay** — Syntax-highlighted stack frames, clickable file links (`vscode://file`)
|
|
91
91
|
- 🔍 **Source Map Stack Mapping** — Maps runtime errors to original TS/JS source lines
|
|
92
92
|
- 💬 **Auto Port Detection** — Prompts when default port 5173 is occupied
|
|
93
93
|
- 🧠 **Smart Config Loader** — Detects project root, compiles `.ts` configs dynamically
|
package/dist/cli/commands/dev.js
CHANGED
|
@@ -19,12 +19,12 @@ const child_process_1 = require("child_process");
|
|
|
19
19
|
const chalk_1 = __importDefault(require("chalk"));
|
|
20
20
|
async function dev() {
|
|
21
21
|
const root = process.cwd();
|
|
22
|
-
// 🧩 Load
|
|
22
|
+
// 🧩 Load config
|
|
23
23
|
const userConfig = await (0, loadConfig_1.loadReactClientConfig)(root);
|
|
24
24
|
const appRoot = path_1.default.resolve(root, userConfig.root || '.');
|
|
25
25
|
const defaultPort = userConfig.server?.port || 5173;
|
|
26
26
|
const outDir = path_1.default.join(appRoot, userConfig.build?.outDir || '.react-client/dev');
|
|
27
|
-
//
|
|
27
|
+
// 🧠 Detect entry (main.tsx / main.jsx)
|
|
28
28
|
const possibleEntries = ['src/main.tsx', 'src/main.jsx'];
|
|
29
29
|
const entry = possibleEntries.map((p) => path_1.default.join(appRoot, p)).find((p) => fs_extra_1.default.existsSync(p));
|
|
30
30
|
if (!entry) {
|
|
@@ -33,7 +33,7 @@ async function dev() {
|
|
|
33
33
|
}
|
|
34
34
|
const indexHtml = path_1.default.join(appRoot, 'index.html');
|
|
35
35
|
await fs_extra_1.default.ensureDir(outDir);
|
|
36
|
-
//
|
|
36
|
+
// 🧠 Detect available port
|
|
37
37
|
const availablePort = await (0, detect_port_1.default)(defaultPort);
|
|
38
38
|
const port = availablePort;
|
|
39
39
|
if (availablePort !== defaultPort) {
|
|
@@ -73,22 +73,21 @@ async function dev() {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
const reactRefreshRuntime = safeResolveReactRefresh();
|
|
76
|
-
//
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// 🌐 Connect server setup
|
|
76
|
+
// 🧠 Dependency Graph + Transform Cache
|
|
77
|
+
const deps = new Map(); // dependency → importers
|
|
78
|
+
const transformCache = new Map();
|
|
79
|
+
async function resolveFile(basePath) {
|
|
80
|
+
if (await fs_extra_1.default.pathExists(basePath))
|
|
81
|
+
return basePath;
|
|
82
|
+
const exts = ['.tsx', '.ts', '.jsx', '.js'];
|
|
83
|
+
for (const ext of exts) {
|
|
84
|
+
const candidate = basePath + ext;
|
|
85
|
+
if (await fs_extra_1.default.pathExists(candidate))
|
|
86
|
+
return candidate;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
// 🌐 connect server
|
|
92
91
|
const app = (0, connect_1.default)();
|
|
93
92
|
// 🛡 Security headers
|
|
94
93
|
app.use((_req, res, next) => {
|
|
@@ -96,9 +95,7 @@ async function dev() {
|
|
|
96
95
|
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
|
|
97
96
|
next();
|
|
98
97
|
});
|
|
99
|
-
//
|
|
100
|
-
const moduleCache = new Map();
|
|
101
|
-
// 1️⃣ Serve react-refresh runtime with safe browser shim
|
|
98
|
+
// 1️⃣ Serve react-refresh runtime with browser shim
|
|
102
99
|
app.use('/@react-refresh', async (_req, res) => {
|
|
103
100
|
const runtime = await fs_extra_1.default.readFile(reactRefreshRuntime, 'utf8');
|
|
104
101
|
const shim = `
|
|
@@ -110,50 +107,63 @@ async function dev() {
|
|
|
110
107
|
res.setHeader('Content-Type', 'application/javascript');
|
|
111
108
|
res.end(shim + '\n' + runtime);
|
|
112
109
|
});
|
|
113
|
-
// 2️⃣
|
|
110
|
+
// 2️⃣ Serve bare modules dynamically (/@modules/)
|
|
114
111
|
app.use('/@modules/', async (req, res, next) => {
|
|
115
|
-
|
|
112
|
+
let id = req.url?.replace(/^\/@modules\//, '');
|
|
116
113
|
if (!id)
|
|
117
114
|
return next();
|
|
118
|
-
|
|
119
|
-
res.setHeader('Content-Type', 'application/javascript');
|
|
120
|
-
res.end(moduleCache.get(id));
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
115
|
+
id = id.replace(/^\/+/, ''); // normalize
|
|
123
116
|
try {
|
|
124
|
-
const
|
|
117
|
+
const entry = require.resolve(id, { paths: [appRoot] });
|
|
125
118
|
const out = await esbuild_1.default.build({
|
|
126
|
-
entryPoints: [
|
|
119
|
+
entryPoints: [entry],
|
|
127
120
|
bundle: true,
|
|
128
121
|
write: false,
|
|
129
122
|
platform: 'browser',
|
|
130
123
|
format: 'esm',
|
|
131
124
|
target: 'es2020',
|
|
132
125
|
});
|
|
133
|
-
const code = out.outputFiles[0].text;
|
|
134
|
-
moduleCache.set(id, code); // ✅ cache module
|
|
135
126
|
res.setHeader('Content-Type', 'application/javascript');
|
|
136
|
-
res.end(
|
|
127
|
+
res.end(out.outputFiles[0].text);
|
|
137
128
|
}
|
|
138
129
|
catch (err) {
|
|
139
130
|
const msg = err instanceof Error ? err.message : String(err);
|
|
140
|
-
console.error(
|
|
131
|
+
console.error(`Failed to resolve module ${id}:`, msg);
|
|
141
132
|
res.writeHead(500);
|
|
142
133
|
res.end(`// Could not resolve module ${id}`);
|
|
143
134
|
}
|
|
144
135
|
});
|
|
145
|
-
// 3️⃣ Serve /src/* files —
|
|
136
|
+
// 3️⃣ Serve /src/* files — with caching, deps tracking, and HMR
|
|
146
137
|
app.use(async (req, res, next) => {
|
|
147
138
|
if (!req.url || !req.url.startsWith('/src/'))
|
|
148
139
|
return next();
|
|
149
140
|
try {
|
|
150
|
-
const
|
|
151
|
-
|
|
141
|
+
const requestPath = decodeURIComponent(req.url.split('?')[0]);
|
|
142
|
+
const filePath = path_1.default.join(appRoot, requestPath);
|
|
143
|
+
const resolvedFile = await resolveFile(filePath);
|
|
144
|
+
if (!resolvedFile)
|
|
152
145
|
return next();
|
|
153
|
-
|
|
154
|
-
|
|
146
|
+
if (transformCache.has(resolvedFile)) {
|
|
147
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
148
|
+
res.end(transformCache.get(resolvedFile));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
let code = await fs_extra_1.default.readFile(resolvedFile, 'utf8');
|
|
152
|
+
const ext = path_1.default.extname(resolvedFile).toLowerCase();
|
|
155
153
|
// 🪄 Rewrite bare imports → /@modules/
|
|
156
|
-
code = code.replace(/from\s+['"]([
|
|
154
|
+
code = code.replace(/from\s+['"]((?![\.\/])[a-zA-Z0-9@/_-]+)['"]/g, (_m, dep) => `from "/@modules/${dep}"`);
|
|
155
|
+
// 🧩 Track dependencies (relative imports)
|
|
156
|
+
const importRegex = /from\s+['"](\.\/[^'"]+|\.{2}\/[^'"]+)['"]/g;
|
|
157
|
+
let match;
|
|
158
|
+
while ((match = importRegex.exec(code)) !== null) {
|
|
159
|
+
const rel = match[1];
|
|
160
|
+
const importer = path_1.default.relative(appRoot, resolvedFile);
|
|
161
|
+
const importedFile = path_1.default.resolve(path_1.default.dirname(resolvedFile), rel);
|
|
162
|
+
const depFile = (await resolveFile(importedFile)) ?? importedFile;
|
|
163
|
+
if (!deps.has(depFile))
|
|
164
|
+
deps.set(depFile, new Set());
|
|
165
|
+
deps.get(depFile).add(importer);
|
|
166
|
+
}
|
|
157
167
|
let loader = 'js';
|
|
158
168
|
if (ext === '.ts')
|
|
159
169
|
loader = 'ts';
|
|
@@ -169,6 +179,7 @@ async function dev() {
|
|
|
169
179
|
jsxFactory: 'React.createElement',
|
|
170
180
|
jsxFragment: 'React.Fragment',
|
|
171
181
|
});
|
|
182
|
+
transformCache.set(resolvedFile, transformed.code);
|
|
172
183
|
res.setHeader('Content-Type', 'application/javascript');
|
|
173
184
|
res.end(transformed.code);
|
|
174
185
|
}
|
|
@@ -179,7 +190,7 @@ async function dev() {
|
|
|
179
190
|
res.end(`// Error: ${msg}`);
|
|
180
191
|
}
|
|
181
192
|
});
|
|
182
|
-
// 4️⃣ Serve index.html
|
|
193
|
+
// 4️⃣ Serve index.html (inject React Refresh + HMR client + overlay)
|
|
183
194
|
app.use(async (req, res, next) => {
|
|
184
195
|
if (req.url === '/' || req.url === '/index.html') {
|
|
185
196
|
if (!fs_extra_1.default.existsSync(indexHtml)) {
|
|
@@ -189,6 +200,49 @@ async function dev() {
|
|
|
189
200
|
}
|
|
190
201
|
let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
|
|
191
202
|
html = html.replace('</body>', `
|
|
203
|
+
<script>
|
|
204
|
+
// 🧩 Lightweight Error Overlay
|
|
205
|
+
(() => {
|
|
206
|
+
const style = document.createElement('style');
|
|
207
|
+
style.textContent = \`
|
|
208
|
+
.rc-overlay {
|
|
209
|
+
position: fixed;
|
|
210
|
+
top: 0; left: 0;
|
|
211
|
+
width: 100vw; height: 100vh;
|
|
212
|
+
background: rgba(0, 0, 0, 0.92);
|
|
213
|
+
color: #ff5555;
|
|
214
|
+
font-family: monospace;
|
|
215
|
+
padding: 2rem;
|
|
216
|
+
overflow: auto;
|
|
217
|
+
z-index: 999999;
|
|
218
|
+
white-space: pre-wrap;
|
|
219
|
+
}
|
|
220
|
+
.rc-overlay h2 {
|
|
221
|
+
color: #ff7575;
|
|
222
|
+
font-size: 1.2rem;
|
|
223
|
+
margin-bottom: 1rem;
|
|
224
|
+
}
|
|
225
|
+
\`;
|
|
226
|
+
document.head.appendChild(style);
|
|
227
|
+
|
|
228
|
+
window.showErrorOverlay = (err) => {
|
|
229
|
+
window.clearErrorOverlay?.();
|
|
230
|
+
const overlay = document.createElement('div');
|
|
231
|
+
overlay.className = 'rc-overlay';
|
|
232
|
+
overlay.innerHTML = '<h2>🚨 React Client Error</h2>' +
|
|
233
|
+
(err.message || err.error || err) + '\\n\\n' + (err.stack || '');
|
|
234
|
+
document.body.appendChild(overlay);
|
|
235
|
+
window.__reactClientOverlay = overlay;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
window.clearErrorOverlay = () => {
|
|
239
|
+
const overlay = window.__reactClientOverlay;
|
|
240
|
+
if (overlay) overlay.remove();
|
|
241
|
+
window.__reactClientOverlay = null;
|
|
242
|
+
};
|
|
243
|
+
})();
|
|
244
|
+
</script>
|
|
245
|
+
|
|
192
246
|
<script type="module">
|
|
193
247
|
import "/@react-refresh";
|
|
194
248
|
const ws = new WebSocket("ws://" + location.host);
|
|
@@ -214,18 +268,10 @@ async function dev() {
|
|
|
214
268
|
res.setHeader('Content-Type', 'text/html');
|
|
215
269
|
res.end(html);
|
|
216
270
|
}
|
|
217
|
-
else
|
|
218
|
-
|
|
219
|
-
if (await fs_extra_1.default.pathExists(filePath)) {
|
|
220
|
-
const content = await fs_extra_1.default.readFile(filePath);
|
|
221
|
-
res.setHeader('Content-Type', 'application/javascript');
|
|
222
|
-
res.end(content);
|
|
223
|
-
}
|
|
224
|
-
else
|
|
225
|
-
next();
|
|
226
|
-
}
|
|
271
|
+
else
|
|
272
|
+
next();
|
|
227
273
|
});
|
|
228
|
-
// 🔁 HMR
|
|
274
|
+
// 🔁 HMR with dependency graph
|
|
229
275
|
const server = http_1.default.createServer(app);
|
|
230
276
|
const wss = new ws_1.WebSocketServer({ server });
|
|
231
277
|
const broadcast = (data) => {
|
|
@@ -233,31 +279,37 @@ async function dev() {
|
|
|
233
279
|
wss.clients.forEach((c) => c.readyState === 1 && c.send(json));
|
|
234
280
|
};
|
|
235
281
|
chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', async (file) => {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
282
|
+
console.log(`🔄 File changed: ${file}`);
|
|
283
|
+
transformCache.delete(file);
|
|
284
|
+
broadcast({ type: 'update', path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/') });
|
|
285
|
+
// Propagate updates to dependents
|
|
286
|
+
const visited = new Set();
|
|
287
|
+
const queue = [file];
|
|
288
|
+
while (queue.length > 0) {
|
|
289
|
+
const dep = queue.pop();
|
|
290
|
+
const importers = deps.get(dep);
|
|
291
|
+
if (!importers)
|
|
292
|
+
continue;
|
|
293
|
+
for (const importer of importers) {
|
|
294
|
+
if (visited.has(importer))
|
|
295
|
+
continue;
|
|
296
|
+
visited.add(importer);
|
|
297
|
+
console.log(chalk_1.default.yellow(`↪️ Updating importer: ${importer}`));
|
|
298
|
+
transformCache.delete(path_1.default.join(appRoot, importer));
|
|
299
|
+
broadcast({ type: 'update', path: '/' + importer.replace(/\\/g, '/') });
|
|
300
|
+
queue.push(path_1.default.join(appRoot, importer));
|
|
247
301
|
}
|
|
248
302
|
}
|
|
249
303
|
});
|
|
250
|
-
// 🟢 Start server
|
|
251
304
|
server.listen(port, async () => {
|
|
252
305
|
const url = `http://localhost:${port}`;
|
|
253
|
-
console.log(chalk_1.default.cyan.bold(
|
|
306
|
+
console.log(chalk_1.default.cyan.bold('\n🚀 React Client Dev Server'));
|
|
254
307
|
console.log(chalk_1.default.gray('───────────────────────────────'));
|
|
255
308
|
console.log(chalk_1.default.green(`⚡ Running at: ${url}`));
|
|
256
309
|
await (0, open_1.default)(url, { newInstance: true });
|
|
257
310
|
});
|
|
258
311
|
process.on('SIGINT', async () => {
|
|
259
312
|
console.log(chalk_1.default.red('\n🛑 Shutting down...'));
|
|
260
|
-
await ctx.dispose();
|
|
261
313
|
server.close();
|
|
262
314
|
process.exit(0);
|
|
263
315
|
});
|