react-client 1.0.23 → 1.0.25
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/dev.js +270 -126
- package/package.json +1 -1
package/dist/cli/commands/dev.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* dev.ts — Vite-like dev server for react-client
|
|
4
|
+
*
|
|
5
|
+
* - prebundles deps into .react-client/deps
|
|
6
|
+
* - serves /@modules/<dep>
|
|
7
|
+
* - serves /src/* with esbuild transform & inline sourcemap
|
|
8
|
+
* - serves /@runtime/overlay -> src/runtime/overlay-runtime.js
|
|
9
|
+
* - /@source-map returns a snippet for overlay mapping
|
|
10
|
+
* - HMR broadcast via BroadcastManager (ws)
|
|
11
|
+
*
|
|
12
|
+
* Keep this file linted & typed. Avoids manual react-dom/client hacks.
|
|
13
|
+
*/
|
|
2
14
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
15
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
16
|
};
|
|
@@ -13,54 +25,53 @@ const prompts_1 = __importDefault(require("prompts"));
|
|
|
13
25
|
const path_1 = __importDefault(require("path"));
|
|
14
26
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
15
27
|
const open_1 = __importDefault(require("open"));
|
|
16
|
-
const child_process_1 = require("child_process");
|
|
17
28
|
const chalk_1 = __importDefault(require("chalk"));
|
|
29
|
+
const child_process_1 = require("child_process");
|
|
18
30
|
const loadConfig_1 = require("../../utils/loadConfig");
|
|
19
31
|
const broadcastManager_1 = require("../../server/broadcastManager");
|
|
20
32
|
async function dev() {
|
|
21
33
|
const root = process.cwd();
|
|
22
|
-
const userConfig = await (0, loadConfig_1.loadReactClientConfig)(root);
|
|
34
|
+
const userConfig = (await (0, loadConfig_1.loadReactClientConfig)(root));
|
|
23
35
|
const appRoot = path_1.default.resolve(root, userConfig.root || '.');
|
|
24
|
-
const defaultPort = userConfig.server?.port
|
|
36
|
+
const defaultPort = userConfig.server?.port ?? 2202;
|
|
37
|
+
// cache dir for prebundled deps
|
|
25
38
|
const cacheDir = path_1.default.join(appRoot, '.react-client', 'deps');
|
|
26
|
-
const pkgFile = path_1.default.join(appRoot, 'package.json');
|
|
27
39
|
await fs_extra_1.default.ensureDir(cacheDir);
|
|
28
|
-
//
|
|
29
|
-
const
|
|
30
|
-
const entry =
|
|
40
|
+
// Detect entry (main.tsx / main.jsx)
|
|
41
|
+
const possible = ['src/main.tsx', 'src/main.jsx'].map((p) => path_1.default.join(appRoot, p));
|
|
42
|
+
const entry = possible.find((p) => fs_extra_1.default.existsSync(p));
|
|
31
43
|
if (!entry) {
|
|
32
|
-
console.error(chalk_1.default.red('❌
|
|
44
|
+
console.error(chalk_1.default.red('❌ Entry not found: src/main.tsx or src/main.jsx'));
|
|
33
45
|
process.exit(1);
|
|
34
46
|
}
|
|
35
47
|
const indexHtml = path_1.default.join(appRoot, 'index.html');
|
|
36
|
-
//
|
|
48
|
+
// Select port
|
|
37
49
|
const availablePort = await (0, detect_port_1.default)(defaultPort);
|
|
38
50
|
const port = availablePort;
|
|
39
51
|
if (availablePort !== defaultPort) {
|
|
40
|
-
const
|
|
52
|
+
const response = await (0, prompts_1.default)({
|
|
41
53
|
type: 'confirm',
|
|
42
54
|
name: 'useNewPort',
|
|
43
55
|
message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
|
|
44
56
|
initial: true,
|
|
45
57
|
});
|
|
46
|
-
if (!
|
|
58
|
+
if (!response.useNewPort) {
|
|
47
59
|
console.log('🛑 Dev server cancelled.');
|
|
48
60
|
process.exit(0);
|
|
49
61
|
}
|
|
50
62
|
}
|
|
51
|
-
//
|
|
63
|
+
// Ensure react-refresh runtime available (used by many templates)
|
|
52
64
|
try {
|
|
53
65
|
require.resolve('react-refresh/runtime');
|
|
54
66
|
}
|
|
55
67
|
catch {
|
|
56
|
-
console.warn(chalk_1.default.yellow('⚠️ react-refresh not found — installing...'));
|
|
68
|
+
console.warn(chalk_1.default.yellow('⚠️ react-refresh not found — installing react-refresh...'));
|
|
57
69
|
(0, child_process_1.execSync)('npm install react-refresh --no-audit --no-fund --silent', {
|
|
58
|
-
cwd:
|
|
70
|
+
cwd: appRoot,
|
|
59
71
|
stdio: 'inherit',
|
|
60
72
|
});
|
|
61
|
-
console.log(chalk_1.default.green('✅ react-refresh installed successfully.'));
|
|
62
73
|
}
|
|
63
|
-
//
|
|
74
|
+
// Plugin system (core + user)
|
|
64
75
|
const corePlugins = [
|
|
65
76
|
{
|
|
66
77
|
name: 'css-hmr',
|
|
@@ -69,10 +80,10 @@ async function dev() {
|
|
|
69
80
|
const escaped = JSON.stringify(code);
|
|
70
81
|
return `
|
|
71
82
|
const css = ${escaped};
|
|
72
|
-
const style = document.createElement(
|
|
83
|
+
const style = document.createElement("style");
|
|
73
84
|
style.textContent = css;
|
|
74
85
|
document.head.appendChild(style);
|
|
75
|
-
import.meta.hot
|
|
86
|
+
import.meta.hot?.accept();
|
|
76
87
|
`;
|
|
77
88
|
}
|
|
78
89
|
return code;
|
|
@@ -81,203 +92,336 @@ async function dev() {
|
|
|
81
92
|
];
|
|
82
93
|
const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
|
|
83
94
|
const plugins = [...corePlugins, ...userPlugins];
|
|
95
|
+
// App + caches
|
|
84
96
|
const app = (0, connect_1.default)();
|
|
85
97
|
const transformCache = new Map();
|
|
86
|
-
//
|
|
98
|
+
// Helper: recursively analyze dependency graph for prebundling (bare imports)
|
|
87
99
|
async function analyzeGraph(file, seen = new Set()) {
|
|
88
100
|
if (seen.has(file))
|
|
89
101
|
return seen;
|
|
90
102
|
seen.add(file);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
try {
|
|
104
|
+
const code = await fs_extra_1.default.readFile(file, 'utf8');
|
|
105
|
+
const matches = [
|
|
106
|
+
...code.matchAll(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g),
|
|
107
|
+
...code.matchAll(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g),
|
|
108
|
+
];
|
|
109
|
+
for (const m of matches) {
|
|
110
|
+
const dep = m[1];
|
|
111
|
+
if (!dep || dep.startsWith('.') || dep.startsWith('/'))
|
|
112
|
+
continue;
|
|
113
|
+
try {
|
|
114
|
+
const resolved = require.resolve(dep, { paths: [appRoot] });
|
|
115
|
+
await analyzeGraph(resolved, seen);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// bare dependency (node_modules) - track name
|
|
119
|
+
seen.add(dep);
|
|
120
|
+
}
|
|
106
121
|
}
|
|
107
122
|
}
|
|
123
|
+
catch {
|
|
124
|
+
// ignore unreadable files
|
|
125
|
+
}
|
|
108
126
|
return seen;
|
|
109
127
|
}
|
|
110
|
-
//
|
|
128
|
+
// Prebundle dependencies into cache dir (parallel)
|
|
111
129
|
async function prebundleDeps(deps) {
|
|
112
130
|
if (!deps.size)
|
|
113
131
|
return;
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
132
|
+
const existingFiles = await fs_extra_1.default.readdir(cacheDir);
|
|
133
|
+
const existing = new Set(existingFiles.map((f) => f.replace(/\.js$/, '')));
|
|
134
|
+
const missing = [...deps].filter((d) => !existing.has(d));
|
|
135
|
+
if (!missing.length)
|
|
118
136
|
return;
|
|
119
|
-
}
|
|
120
137
|
console.log(chalk_1.default.cyan('📦 Prebundling:'), missing.join(', '));
|
|
121
138
|
await Promise.all(missing.map(async (dep) => {
|
|
122
139
|
try {
|
|
123
|
-
const
|
|
124
|
-
const outFile = path_1.default.join(cacheDir, dep + '.js');
|
|
140
|
+
const entryPoint = require.resolve(dep, { paths: [appRoot] });
|
|
141
|
+
const outFile = path_1.default.join(cacheDir, dep.replace(/\//g, '_') + '.js');
|
|
125
142
|
await esbuild_1.default.build({
|
|
126
|
-
entryPoints: [
|
|
143
|
+
entryPoints: [entryPoint],
|
|
127
144
|
bundle: true,
|
|
128
145
|
platform: 'browser',
|
|
129
146
|
format: 'esm',
|
|
130
147
|
outfile: outFile,
|
|
131
148
|
write: true,
|
|
132
|
-
target: 'es2020',
|
|
149
|
+
target: ['es2020'],
|
|
133
150
|
});
|
|
134
151
|
console.log(chalk_1.default.green(`✅ Cached ${dep}`));
|
|
135
152
|
}
|
|
136
153
|
catch (err) {
|
|
137
|
-
|
|
138
|
-
console.warn(chalk_1.default.yellow(`⚠️ Skipped ${dep}: ${e.message}`));
|
|
154
|
+
console.warn(chalk_1.default.yellow(`⚠️ Skipped ${dep}: ${err.message}`));
|
|
139
155
|
}
|
|
140
156
|
}));
|
|
141
157
|
}
|
|
142
|
-
//
|
|
143
|
-
const
|
|
144
|
-
await prebundleDeps(
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
// Build initial prebundle graph from entry
|
|
159
|
+
const depsSet = await analyzeGraph(entry);
|
|
160
|
+
await prebundleDeps(depsSet);
|
|
161
|
+
// Watch package.json for changes to re-prebundle
|
|
162
|
+
const pkgPath = path_1.default.join(appRoot, 'package.json');
|
|
163
|
+
if (await fs_extra_1.default.pathExists(pkgPath)) {
|
|
164
|
+
chokidar_1.default.watch(pkgPath).on('change', async () => {
|
|
165
|
+
console.log(chalk_1.default.yellow('📦 package.json changed — rebuilding prebundle...'));
|
|
166
|
+
const newDeps = await analyzeGraph(entry);
|
|
167
|
+
await prebundleDeps(newDeps);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// --- Serve /@modules/<dep> (prebundled or on-demand esbuild bundle)
|
|
171
|
+
app.use(async (req, res, next) => {
|
|
172
|
+
const url = req.url ?? '';
|
|
173
|
+
if (!url.startsWith('/@modules/'))
|
|
155
174
|
return next();
|
|
175
|
+
const id = url.replace(/^\/@modules\//, '');
|
|
176
|
+
if (!id) {
|
|
177
|
+
res.writeHead(400);
|
|
178
|
+
return res.end('// invalid module');
|
|
179
|
+
}
|
|
156
180
|
try {
|
|
157
|
-
const cacheFile = path_1.default.join(cacheDir, id.replace(
|
|
181
|
+
const cacheFile = path_1.default.join(cacheDir, id.replace(/[\\/]/g, '_') + '.js');
|
|
158
182
|
if (await fs_extra_1.default.pathExists(cacheFile)) {
|
|
159
183
|
res.setHeader('Content-Type', 'application/javascript');
|
|
160
|
-
return res.end(await fs_extra_1.default.readFile(cacheFile));
|
|
184
|
+
return res.end(await fs_extra_1.default.readFile(cacheFile, 'utf8'));
|
|
185
|
+
}
|
|
186
|
+
// 🧠 Handle subpaths correctly: react-dom/client, react/jsx-runtime, etc.
|
|
187
|
+
let entryPath = null;
|
|
188
|
+
try {
|
|
189
|
+
entryPath = require.resolve(id, { paths: [appRoot] });
|
|
161
190
|
}
|
|
162
|
-
|
|
191
|
+
catch {
|
|
192
|
+
// Fallback: handle packages with subpaths dynamically
|
|
193
|
+
const parts = id.split('/');
|
|
194
|
+
if (parts.length > 1) {
|
|
195
|
+
const pkgRoot = parts[0].startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
196
|
+
const subPath = parts.slice(pkgRoot.startsWith('@') ? 2 : 1).join('/');
|
|
197
|
+
const pkgDir = path_1.default.dirname(require.resolve(`${pkgRoot}/package.json`, { paths: [appRoot] }));
|
|
198
|
+
// resolve full subpath from package root
|
|
199
|
+
const tryPath = path_1.default.join(pkgDir, subPath);
|
|
200
|
+
if (await fs_extra_1.default.pathExists(tryPath + '.js'))
|
|
201
|
+
entryPath = tryPath + '.js';
|
|
202
|
+
else if (await fs_extra_1.default.pathExists(tryPath + '.mjs'))
|
|
203
|
+
entryPath = tryPath + '.mjs';
|
|
204
|
+
else if (await fs_extra_1.default.pathExists(tryPath + '/index.js'))
|
|
205
|
+
entryPath = tryPath + '/index.js';
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!entryPath)
|
|
209
|
+
throw new Error(`Could not resolve ${id}`);
|
|
163
210
|
const result = await esbuild_1.default.build({
|
|
164
211
|
entryPoints: [entryPath],
|
|
165
212
|
bundle: true,
|
|
166
213
|
platform: 'browser',
|
|
167
214
|
format: 'esm',
|
|
168
|
-
target: 'es2020',
|
|
169
215
|
write: false,
|
|
216
|
+
target: ['es2020'],
|
|
170
217
|
});
|
|
171
|
-
const
|
|
172
|
-
await fs_extra_1.default.writeFile(cacheFile,
|
|
218
|
+
const output = result.outputFiles?.[0]?.text ?? '';
|
|
219
|
+
await fs_extra_1.default.writeFile(cacheFile, output, 'utf8');
|
|
173
220
|
res.setHeader('Content-Type', 'application/javascript');
|
|
174
|
-
res.end(
|
|
221
|
+
res.end(output);
|
|
175
222
|
}
|
|
176
223
|
catch (err) {
|
|
177
|
-
const e = err;
|
|
178
|
-
console.error(chalk_1.default.red(`❌ Failed to load module ${id}: ${e.message}`));
|
|
179
224
|
res.writeHead(500);
|
|
180
|
-
res.end(`// Failed to resolve module ${id}: ${
|
|
225
|
+
res.end(`// Failed to resolve module ${id}: ${err.message}`);
|
|
181
226
|
}
|
|
182
227
|
});
|
|
183
|
-
//
|
|
228
|
+
// --- Serve runtime overlay (local file) so overlay-runtime.js is loaded automatically
|
|
184
229
|
app.use(async (req, res, next) => {
|
|
185
|
-
if (
|
|
230
|
+
if (req.url === '/@runtime/overlay') {
|
|
231
|
+
const overlayPath = path_1.default.join(appRoot, 'src/runtime/overlay-runtime.js');
|
|
232
|
+
if (!(await fs_extra_1.default.pathExists(overlayPath))) {
|
|
233
|
+
res.writeHead(404);
|
|
234
|
+
return res.end('// overlay-runtime.js not found');
|
|
235
|
+
}
|
|
236
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
237
|
+
return res.end(await fs_extra_1.default.readFile(overlayPath, 'utf8'));
|
|
238
|
+
}
|
|
239
|
+
next();
|
|
240
|
+
});
|
|
241
|
+
// --- minimal /@source-map: return snippet around requested line of original source file
|
|
242
|
+
app.use((async (req, res, next) => {
|
|
243
|
+
const url = req.url ?? '';
|
|
244
|
+
if (!url.startsWith('/@source-map'))
|
|
245
|
+
return next();
|
|
246
|
+
// expected query: ?file=/src/xyz.tsx&line=12&column=3
|
|
247
|
+
try {
|
|
248
|
+
const full = req.url ?? '';
|
|
249
|
+
const parsed = new URL(full, `http://localhost:${port}`);
|
|
250
|
+
const file = parsed.searchParams.get('file') ?? '';
|
|
251
|
+
const lineStr = parsed.searchParams.get('line') ?? '0';
|
|
252
|
+
const lineNum = Number(lineStr) || 0;
|
|
253
|
+
if (!file) {
|
|
254
|
+
res.writeHead(400);
|
|
255
|
+
return res.end('{}');
|
|
256
|
+
}
|
|
257
|
+
const filePath = path_1.default.join(appRoot, file.startsWith('/') ? file.slice(1) : file);
|
|
258
|
+
if (!(await fs_extra_1.default.pathExists(filePath))) {
|
|
259
|
+
res.writeHead(404);
|
|
260
|
+
return res.end('{}');
|
|
261
|
+
}
|
|
262
|
+
const src = await fs_extra_1.default.readFile(filePath, 'utf8');
|
|
263
|
+
const lines = src.split(/\r?\n/);
|
|
264
|
+
const start = Math.max(0, lineNum - 3 - 1);
|
|
265
|
+
const end = Math.min(lines.length, lineNum + 2);
|
|
266
|
+
const snippet = lines
|
|
267
|
+
.slice(start, end)
|
|
268
|
+
.map((l, i) => {
|
|
269
|
+
const ln = start + i + 1;
|
|
270
|
+
return `<span class="line-number">${ln}</span> ${l
|
|
271
|
+
.replace(/</g, '<')
|
|
272
|
+
.replace(/>/g, '>')}`;
|
|
273
|
+
})
|
|
274
|
+
.join('\n');
|
|
275
|
+
res.setHeader('Content-Type', 'application/json');
|
|
276
|
+
res.end(JSON.stringify({ source: file, line: lineNum, column: 0, snippet }));
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
res.writeHead(500);
|
|
280
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
281
|
+
}
|
|
282
|
+
}));
|
|
283
|
+
// --- Serve /src/* files (on-the-fly transform + bare import rewrite)
|
|
284
|
+
app.use((async (req, res, next) => {
|
|
285
|
+
const url = req.url ?? '';
|
|
286
|
+
if (!url.startsWith('/src/') && !url.endsWith('.css'))
|
|
186
287
|
return next();
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
288
|
+
const raw = decodeURIComponent((req.url ?? '').split('?')[0]);
|
|
289
|
+
const filePath = path_1.default.join(appRoot, raw.replace(/^\//, ''));
|
|
290
|
+
// Try file extensions if not exact file
|
|
291
|
+
const exts = ['', '.tsx', '.ts', '.jsx', '.js', '.css'];
|
|
292
|
+
let found = '';
|
|
293
|
+
for (const ext of exts) {
|
|
191
294
|
if (await fs_extra_1.default.pathExists(filePath + ext)) {
|
|
192
|
-
filePath
|
|
295
|
+
found = filePath + ext;
|
|
193
296
|
break;
|
|
194
297
|
}
|
|
195
298
|
}
|
|
196
|
-
if (!
|
|
299
|
+
if (!found)
|
|
197
300
|
return next();
|
|
198
301
|
try {
|
|
199
|
-
let code = await fs_extra_1.default.readFile(
|
|
200
|
-
//
|
|
302
|
+
let code = await fs_extra_1.default.readFile(found, 'utf8');
|
|
303
|
+
// rewrite bare imports -> /@modules/<dep>
|
|
201
304
|
code = code
|
|
202
305
|
.replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
|
|
203
306
|
.replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
307
|
+
// run plugin transforms
|
|
308
|
+
for (const p of plugins) {
|
|
309
|
+
if (p.onTransform) {
|
|
310
|
+
// plugin may return transformed code
|
|
311
|
+
// keep typed as string
|
|
312
|
+
// eslint-disable-next-line no-await-in-loop
|
|
313
|
+
const out = await p.onTransform(code, found);
|
|
314
|
+
if (typeof out === 'string')
|
|
315
|
+
code = out;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// choose loader by extension
|
|
319
|
+
const ext = path_1.default.extname(found).toLowerCase();
|
|
320
|
+
const loader = ext === '.ts' ? 'ts' : ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'js';
|
|
215
321
|
const result = await esbuild_1.default.transform(code, {
|
|
216
322
|
loader,
|
|
217
323
|
sourcemap: 'inline',
|
|
218
|
-
target: 'es2020',
|
|
324
|
+
target: ['es2020'],
|
|
219
325
|
});
|
|
220
|
-
transformCache.set(
|
|
326
|
+
transformCache.set(found, result.code);
|
|
221
327
|
res.setHeader('Content-Type', 'application/javascript');
|
|
222
328
|
res.end(result.code);
|
|
223
329
|
}
|
|
224
330
|
catch (err) {
|
|
225
331
|
const e = err;
|
|
226
|
-
console.error(chalk_1.default.red(`⚠️ Transform failed: ${e.message}`));
|
|
227
332
|
res.writeHead(500);
|
|
228
|
-
res.end(`//
|
|
333
|
+
res.end(`// transform error: ${e.message}`);
|
|
229
334
|
}
|
|
230
|
-
});
|
|
231
|
-
//
|
|
232
|
-
app.use(async (req, res, next) => {
|
|
233
|
-
|
|
335
|
+
}));
|
|
336
|
+
// --- Serve index.html with overlay + HMR client injection
|
|
337
|
+
app.use((async (req, res, next) => {
|
|
338
|
+
const url = req.url ?? '';
|
|
339
|
+
if (url !== '/' && url !== '/index.html')
|
|
234
340
|
return next();
|
|
235
|
-
if (!fs_extra_1.default.
|
|
341
|
+
if (!(await fs_extra_1.default.pathExists(indexHtml))) {
|
|
236
342
|
res.writeHead(404);
|
|
237
343
|
return res.end('index.html not found');
|
|
238
344
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
345
|
+
try {
|
|
346
|
+
let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
|
|
347
|
+
// inject overlay runtime and HMR client if not already present
|
|
348
|
+
if (!html.includes('/@runtime/overlay')) {
|
|
349
|
+
html = html.replace('</body>', `\n<script type="module" src="/@runtime/overlay"></script>\n<script type="module">
|
|
350
|
+
const ws = new WebSocket("ws://" + location.host);
|
|
351
|
+
ws.onmessage = (e) => {
|
|
352
|
+
const msg = JSON.parse(e.data);
|
|
353
|
+
if (msg.type === "reload") location.reload();
|
|
354
|
+
if (msg.type === "error") window.showErrorOverlay?.(msg);
|
|
355
|
+
if (msg.type === "update") {
|
|
356
|
+
window.clearErrorOverlay?.();
|
|
357
|
+
import(msg.path + "?t=" + Date.now());
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
</script>\n</body>`);
|
|
361
|
+
}
|
|
362
|
+
res.setHeader('Content-Type', 'text/html');
|
|
363
|
+
res.end(html);
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
res.writeHead(500);
|
|
367
|
+
res.end(`// html read error: ${err.message}`);
|
|
368
|
+
}
|
|
369
|
+
}));
|
|
370
|
+
// --- HMR WebSocket server
|
|
259
371
|
const server = http_1.default.createServer(app);
|
|
260
372
|
const broadcaster = new broadcastManager_1.BroadcastManager(server);
|
|
261
|
-
|
|
262
|
-
|
|
373
|
+
// Watch files and trigger plugin onHotUpdate + broadcast HMR message
|
|
374
|
+
const watcher = chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true });
|
|
375
|
+
watcher.on('change', async (file) => {
|
|
263
376
|
transformCache.delete(file);
|
|
264
|
-
|
|
265
|
-
|
|
377
|
+
// plugin hook onHotUpdate optionally
|
|
378
|
+
for (const p of plugins) {
|
|
379
|
+
if (p.onHotUpdate) {
|
|
380
|
+
try {
|
|
381
|
+
// allow plugin to broadcast via a simple function
|
|
382
|
+
// plugin gets { broadcast }
|
|
383
|
+
// plugin signature: onHotUpdate(file, { broadcast })
|
|
384
|
+
// eslint-disable-next-line no-await-in-loop
|
|
385
|
+
await p.onHotUpdate(file, {
|
|
386
|
+
broadcast: (msg) => {
|
|
387
|
+
broadcaster.broadcast(msg);
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
// plugin errors shouldn't crash server
|
|
393
|
+
// eslint-disable-next-line no-console
|
|
394
|
+
console.warn('plugin onHotUpdate error:', err.message);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// default: broadcast update for changed file
|
|
266
399
|
broadcaster.broadcast({
|
|
267
400
|
type: 'update',
|
|
268
401
|
path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/'),
|
|
269
402
|
});
|
|
270
403
|
});
|
|
404
|
+
// start server
|
|
271
405
|
server.listen(port, async () => {
|
|
272
406
|
const url = `http://localhost:${port}`;
|
|
273
407
|
console.log(chalk_1.default.cyan.bold('\n🚀 React Client Dev Server'));
|
|
274
|
-
console.log(chalk_1.default.gray('──────────────────────────────'));
|
|
275
408
|
console.log(chalk_1.default.green(`⚡ Running at: ${url}`));
|
|
276
|
-
|
|
409
|
+
if (userConfig.server?.open !== false) {
|
|
410
|
+
// open default browser
|
|
411
|
+
try {
|
|
412
|
+
await (0, open_1.default)(url);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
// ignore open errors
|
|
416
|
+
}
|
|
417
|
+
}
|
|
277
418
|
});
|
|
278
|
-
|
|
419
|
+
// graceful shutdown
|
|
420
|
+
process.on('SIGINT', async () => {
|
|
279
421
|
console.log(chalk_1.default.red('\n🛑 Shutting down...'));
|
|
422
|
+
watcher.close();
|
|
280
423
|
broadcaster.close();
|
|
424
|
+
server.close();
|
|
281
425
|
process.exit(0);
|
|
282
426
|
});
|
|
283
427
|
}
|