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