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