react-client 1.0.22 ā 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 +278 -205
- package/package.json +1 -1
package/dist/cli/commands/dev.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
11
13
|
*/
|
|
12
14
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
15
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -23,69 +25,54 @@ const prompts_1 = __importDefault(require("prompts"));
|
|
|
23
25
|
const path_1 = __importDefault(require("path"));
|
|
24
26
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
25
27
|
const open_1 = __importDefault(require("open"));
|
|
26
|
-
const child_process_1 = require("child_process");
|
|
27
28
|
const chalk_1 = __importDefault(require("chalk"));
|
|
28
|
-
const
|
|
29
|
-
const crypto_1 = __importDefault(require("crypto"));
|
|
29
|
+
const child_process_1 = require("child_process");
|
|
30
30
|
const loadConfig_1 = require("../../utils/loadConfig");
|
|
31
31
|
const broadcastManager_1 = require("../../server/broadcastManager");
|
|
32
|
-
const
|
|
33
|
-
const getMimeType = (file) => {
|
|
34
|
-
const ext = path_1.default.extname(file).toLowerCase();
|
|
35
|
-
const mime = {
|
|
36
|
-
'.ico': 'image/x-icon',
|
|
37
|
-
'.png': 'image/png',
|
|
38
|
-
'.jpg': 'image/jpeg',
|
|
39
|
-
'.jpeg': 'image/jpeg',
|
|
40
|
-
'.gif': 'image/gif',
|
|
41
|
-
'.svg': 'image/svg+xml',
|
|
42
|
-
'.webp': 'image/webp',
|
|
43
|
-
'.json': 'application/json',
|
|
44
|
-
'.txt': 'text/plain',
|
|
45
|
-
'.js': 'application/javascript',
|
|
46
|
-
'.mjs': 'application/javascript',
|
|
47
|
-
'.css': 'text/css',
|
|
48
|
-
'.html': 'text/html',
|
|
49
|
-
};
|
|
50
|
-
return mime[ext] || 'application/octet-stream';
|
|
51
|
-
};
|
|
32
|
+
const RUNTIME_OVERLAY = '/src/runtime/overlay-runtime.js';
|
|
52
33
|
async function dev() {
|
|
53
34
|
const root = process.cwd();
|
|
54
|
-
const userConfig = await (0, loadConfig_1.loadReactClientConfig)(root);
|
|
35
|
+
const userConfig = (await (0, loadConfig_1.loadReactClientConfig)(root));
|
|
55
36
|
const appRoot = path_1.default.resolve(root, userConfig.root || '.');
|
|
56
|
-
const defaultPort = userConfig.server?.port
|
|
37
|
+
const defaultPort = userConfig.server?.port ?? 2202;
|
|
38
|
+
// cache dir for prebundled deps
|
|
57
39
|
const cacheDir = path_1.default.join(appRoot, '.react-client', 'deps');
|
|
58
|
-
const pkgFile = path_1.default.join(appRoot, 'package.json');
|
|
59
|
-
const indexHtml = path_1.default.join(appRoot, 'index.html');
|
|
60
40
|
await fs_extra_1.default.ensureDir(cacheDir);
|
|
61
|
-
//
|
|
62
|
-
const
|
|
63
|
-
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));
|
|
64
44
|
if (!entry) {
|
|
65
|
-
console.error(chalk_1.default.red('ā
|
|
45
|
+
console.error(chalk_1.default.red('ā Entry not found: src/main.tsx or src/main.jsx'));
|
|
66
46
|
process.exit(1);
|
|
67
47
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
48
|
+
const indexHtml = path_1.default.join(appRoot, 'index.html');
|
|
49
|
+
// Select port
|
|
50
|
+
const availablePort = await (0, detect_port_1.default)(defaultPort);
|
|
51
|
+
const port = availablePort;
|
|
52
|
+
if (availablePort !== defaultPort) {
|
|
53
|
+
const response = await (0, prompts_1.default)({
|
|
72
54
|
type: 'confirm',
|
|
73
55
|
name: 'useNewPort',
|
|
74
|
-
message: `Port ${defaultPort} is occupied. Use ${
|
|
56
|
+
message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
|
|
75
57
|
initial: true,
|
|
76
58
|
});
|
|
77
|
-
if (!
|
|
59
|
+
if (!response.useNewPort) {
|
|
60
|
+
console.log('š Dev server cancelled.');
|
|
78
61
|
process.exit(0);
|
|
62
|
+
}
|
|
79
63
|
}
|
|
80
|
-
//
|
|
64
|
+
// Ensure react-refresh runtime available (used by many templates)
|
|
81
65
|
try {
|
|
82
66
|
require.resolve('react-refresh/runtime');
|
|
83
67
|
}
|
|
84
68
|
catch {
|
|
85
|
-
console.
|
|
86
|
-
(0, child_process_1.execSync)('npm
|
|
69
|
+
console.warn(chalk_1.default.yellow('ā ļø react-refresh not found ā installing react-refresh...'));
|
|
70
|
+
(0, child_process_1.execSync)('npm install react-refresh --no-audit --no-fund --silent', {
|
|
71
|
+
cwd: appRoot,
|
|
72
|
+
stdio: 'inherit',
|
|
73
|
+
});
|
|
87
74
|
}
|
|
88
|
-
//
|
|
75
|
+
// Plugin system (core + user)
|
|
89
76
|
const corePlugins = [
|
|
90
77
|
{
|
|
91
78
|
name: 'css-hmr',
|
|
@@ -94,10 +81,10 @@ async function dev() {
|
|
|
94
81
|
const escaped = JSON.stringify(code);
|
|
95
82
|
return `
|
|
96
83
|
const css = ${escaped};
|
|
97
|
-
const style = document.createElement(
|
|
84
|
+
const style = document.createElement("style");
|
|
98
85
|
style.textContent = css;
|
|
99
86
|
document.head.appendChild(style);
|
|
100
|
-
import.meta.hot
|
|
87
|
+
import.meta.hot?.accept();
|
|
101
88
|
`;
|
|
102
89
|
}
|
|
103
90
|
return code;
|
|
@@ -106,230 +93,316 @@ async function dev() {
|
|
|
106
93
|
];
|
|
107
94
|
const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
|
|
108
95
|
const plugins = [...corePlugins, ...userPlugins];
|
|
96
|
+
// App + caches
|
|
109
97
|
const app = (0, connect_1.default)();
|
|
110
|
-
const server = http_1.default.createServer(app);
|
|
111
|
-
const broadcaster = new broadcastManager_1.BroadcastManager(server);
|
|
112
98
|
const transformCache = new Map();
|
|
113
|
-
//
|
|
114
|
-
async function
|
|
115
|
-
if (
|
|
116
|
-
return;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
99
|
+
// Helper: recursively analyze dependency graph for prebundling (bare imports)
|
|
100
|
+
async function analyzeGraph(file, seen = new Set()) {
|
|
101
|
+
if (seen.has(file))
|
|
102
|
+
return seen;
|
|
103
|
+
seen.add(file);
|
|
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
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// ignore unreadable files
|
|
126
|
+
}
|
|
127
|
+
return seen;
|
|
128
|
+
}
|
|
129
|
+
// Prebundle dependencies into cache dir (parallel)
|
|
130
|
+
async function prebundleDeps(deps) {
|
|
131
|
+
if (!deps.size)
|
|
120
132
|
return;
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
if (
|
|
125
|
-
prevHash = (await fs_extra_1.default.readJSON(metaFile)).hash;
|
|
126
|
-
if (prevHash === hash)
|
|
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)
|
|
127
137
|
return;
|
|
128
|
-
console.log(chalk_1.default.cyan('š¦
|
|
129
|
-
await Promise.all(
|
|
138
|
+
console.log(chalk_1.default.cyan('š¦ Prebundling:'), missing.join(', '));
|
|
139
|
+
await Promise.all(missing.map(async (dep) => {
|
|
130
140
|
try {
|
|
131
|
-
const
|
|
132
|
-
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');
|
|
133
143
|
await esbuild_1.default.build({
|
|
134
|
-
entryPoints: [
|
|
144
|
+
entryPoints: [entryPoint],
|
|
135
145
|
bundle: true,
|
|
136
146
|
platform: 'browser',
|
|
137
147
|
format: 'esm',
|
|
138
|
-
target: 'es2020',
|
|
139
148
|
outfile: outFile,
|
|
140
149
|
write: true,
|
|
150
|
+
target: ['es2020'],
|
|
141
151
|
});
|
|
142
|
-
const content = await fs_extra_1.default.readFile(outFile);
|
|
143
|
-
await fs_extra_1.default.writeFile(outFile + '.gz', zlib_1.default.gzipSync(content));
|
|
144
|
-
await fs_extra_1.default.writeFile(outFile + '.br', zlib_1.default.brotliCompressSync(content));
|
|
145
152
|
console.log(chalk_1.default.green(`ā
Cached ${dep}`));
|
|
146
153
|
}
|
|
147
|
-
catch (
|
|
148
|
-
const err = e;
|
|
154
|
+
catch (err) {
|
|
149
155
|
console.warn(chalk_1.default.yellow(`ā ļø Skipped ${dep}: ${err.message}`));
|
|
150
156
|
}
|
|
151
157
|
}));
|
|
152
|
-
await fs_extra_1.default.writeJSON(metaFile, { hash });
|
|
153
158
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
loader: 'js',
|
|
171
|
-
sourcemap: 'inline',
|
|
172
|
-
target: 'es2020',
|
|
173
|
-
});
|
|
174
|
-
res.setHeader('Content-Type', 'application/javascript');
|
|
175
|
-
res.end(result.code);
|
|
176
|
-
}
|
|
177
|
-
catch (err) {
|
|
178
|
-
const e = err;
|
|
179
|
-
console.error(chalk_1.default.red(`ā Failed to load overlay runtime: ${e.message}`));
|
|
180
|
-
res.writeHead(500);
|
|
181
|
-
res.end(`// Failed to load overlay runtime: ${e.message}`);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
// š§ Serve /@modules/
|
|
185
|
-
app.use('/@modules/', async (req, res, next) => {
|
|
186
|
-
const id = req.url?.replace(/^\/(@modules\/)?/, '');
|
|
187
|
-
if (!id)
|
|
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/'))
|
|
188
175
|
return next();
|
|
176
|
+
const id = url.replace(/^\/@modules\//, '');
|
|
177
|
+
if (!id) {
|
|
178
|
+
res.writeHead(400);
|
|
179
|
+
return res.end('// invalid module');
|
|
180
|
+
}
|
|
189
181
|
try {
|
|
190
|
-
const cacheFile = path_1.default.join(cacheDir, id.replace(
|
|
182
|
+
const cacheFile = path_1.default.join(cacheDir, id.replace(/[\\/]/g, '_') + '.js');
|
|
191
183
|
if (await fs_extra_1.default.pathExists(cacheFile)) {
|
|
192
184
|
res.setHeader('Content-Type', 'application/javascript');
|
|
193
|
-
|
|
185
|
+
res.end(await fs_extra_1.default.readFile(cacheFile, 'utf8'));
|
|
186
|
+
return;
|
|
194
187
|
}
|
|
195
|
-
|
|
188
|
+
// Resolve and bundle on-demand
|
|
189
|
+
const entryResolved = require.resolve(id, { paths: [appRoot] });
|
|
196
190
|
const result = await esbuild_1.default.build({
|
|
197
|
-
entryPoints: [
|
|
191
|
+
entryPoints: [entryResolved],
|
|
198
192
|
bundle: true,
|
|
193
|
+
write: false,
|
|
199
194
|
platform: 'browser',
|
|
200
195
|
format: 'esm',
|
|
201
|
-
target: 'es2020',
|
|
202
|
-
write: false,
|
|
196
|
+
target: ['es2020'],
|
|
203
197
|
});
|
|
204
|
-
const
|
|
205
|
-
|
|
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');
|
|
206
201
|
res.setHeader('Content-Type', 'application/javascript');
|
|
207
|
-
res.end(
|
|
202
|
+
res.end(output);
|
|
208
203
|
}
|
|
209
|
-
catch (
|
|
210
|
-
const err = e;
|
|
211
|
-
console.error(chalk_1.default.red(`ā Failed to load module ${id}: ${err.message}`));
|
|
204
|
+
catch (err) {
|
|
212
205
|
res.writeHead(500);
|
|
213
206
|
res.end(`// Failed to resolve module ${id}: ${err.message}`);
|
|
214
207
|
}
|
|
215
|
-
});
|
|
216
|
-
//
|
|
217
|
-
app.use(async (req, res, next) => {
|
|
218
|
-
|
|
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')
|
|
219
213
|
return next();
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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'))
|
|
226
|
+
return next();
|
|
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('{}');
|
|
229
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 }));
|
|
230
258
|
}
|
|
231
|
-
|
|
232
|
-
res.writeHead(
|
|
233
|
-
|
|
259
|
+
catch (err) {
|
|
260
|
+
res.writeHead(500);
|
|
261
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
234
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) {
|
|
275
|
+
if (await fs_extra_1.default.pathExists(filePath + ext)) {
|
|
276
|
+
found = filePath + ext;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (!found)
|
|
281
|
+
return next();
|
|
235
282
|
try {
|
|
236
|
-
let code = await fs_extra_1.default.readFile(
|
|
237
|
-
//
|
|
283
|
+
let code = await fs_extra_1.default.readFile(found, 'utf8');
|
|
284
|
+
// rewrite bare imports -> /@modules/<dep>
|
|
238
285
|
code = code
|
|
239
286
|
.replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
|
|
240
287
|
.replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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';
|
|
252
302
|
const result = await esbuild_1.default.transform(code, {
|
|
253
303
|
loader,
|
|
254
304
|
sourcemap: 'inline',
|
|
255
|
-
target: 'es2020',
|
|
305
|
+
target: ['es2020'],
|
|
256
306
|
});
|
|
307
|
+
transformCache.set(found, result.code);
|
|
257
308
|
res.setHeader('Content-Type', 'application/javascript');
|
|
258
309
|
res.end(result.code);
|
|
259
310
|
}
|
|
260
311
|
catch (err) {
|
|
261
312
|
const e = err;
|
|
262
|
-
console.error(chalk_1.default.red(`ā ļø Transform failed: ${e.message}`));
|
|
263
313
|
res.writeHead(500);
|
|
264
|
-
res.end(`//
|
|
314
|
+
res.end(`// transform error: ${e.message}`);
|
|
265
315
|
}
|
|
266
|
-
});
|
|
267
|
-
//
|
|
268
|
-
app.use(async (req, res, next) => {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const publicDir = path_1.default.join(appRoot, 'public');
|
|
272
|
-
const targetFile = path_1.default.join(publicDir, decodeURIComponent(req.url.split('?')[0]));
|
|
273
|
-
if (!(await fs_extra_1.default.pathExists(targetFile)))
|
|
274
|
-
return next();
|
|
275
|
-
const stat = await fs_extra_1.default.stat(targetFile);
|
|
276
|
-
if (!stat.isFile())
|
|
277
|
-
return next();
|
|
278
|
-
res.setHeader('Content-Type', getMimeType(targetFile));
|
|
279
|
-
fs_extra_1.default.createReadStream(targetFile).pipe(res);
|
|
280
|
-
});
|
|
281
|
-
// š§© Serve index.html + overlay + HMR
|
|
282
|
-
app.use(async (req, res, next) => {
|
|
283
|
-
if (req.url !== '/' && req.url !== '/index.html')
|
|
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')
|
|
284
321
|
return next();
|
|
285
322
|
if (!(await fs_extra_1.default.pathExists(indexHtml))) {
|
|
286
323
|
res.writeHead(404);
|
|
287
324
|
return res.end('index.html not found');
|
|
288
325
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
352
|
+
const server = http_1.default.createServer(app);
|
|
353
|
+
const broadcaster = new broadcastManager_1.BroadcastManager(server);
|
|
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) => {
|
|
311
357
|
transformCache.delete(file);
|
|
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
|
|
312
380
|
broadcaster.broadcast({
|
|
313
381
|
type: 'update',
|
|
314
382
|
path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/'),
|
|
315
383
|
});
|
|
316
384
|
});
|
|
317
|
-
|
|
318
|
-
.watch(path_1.default.join(appRoot, 'src/runtime/overlay-runtime.js'), { ignoreInitial: true })
|
|
319
|
-
.on('change', () => {
|
|
320
|
-
console.log(chalk_1.default.magenta('ā»ļø Overlay runtime updated ā reloading browser...'));
|
|
321
|
-
broadcaster.broadcast({ type: 'reload' });
|
|
322
|
-
});
|
|
385
|
+
// start server
|
|
323
386
|
server.listen(port, async () => {
|
|
324
387
|
const url = `http://localhost:${port}`;
|
|
325
388
|
console.log(chalk_1.default.cyan.bold('\nš React Client Dev Server'));
|
|
326
|
-
console.log(chalk_1.default.gray('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
327
389
|
console.log(chalk_1.default.green(`ā” Running at: ${url}`));
|
|
328
|
-
|
|
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
|
+
}
|
|
329
399
|
});
|
|
330
|
-
|
|
400
|
+
// graceful shutdown
|
|
401
|
+
process.on('SIGINT', async () => {
|
|
331
402
|
console.log(chalk_1.default.red('\nš Shutting down...'));
|
|
403
|
+
watcher.close();
|
|
332
404
|
broadcaster.close();
|
|
405
|
+
server.close();
|
|
333
406
|
process.exit(0);
|
|
334
407
|
});
|
|
335
408
|
}
|