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