react-client 1.0.20 ā 1.0.21
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/README.md +3 -3
- package/dist/cli/commands/dev.js +205 -173
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/preview.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ import { defineConfig } from 'react-client/config';
|
|
|
59
59
|
|
|
60
60
|
export default defineConfig({
|
|
61
61
|
root: './src',
|
|
62
|
-
server: { port:
|
|
62
|
+
server: { port: 2202 },
|
|
63
63
|
build: { outDir: '.react-client/build' }
|
|
64
64
|
});
|
|
65
65
|
```
|
|
@@ -89,7 +89,7 @@ Each template is pre-configured for esbuild, HMR, and fast bootstrapping.
|
|
|
89
89
|
- š **React Fast Refresh (HMR)** ā State-preserving reloads
|
|
90
90
|
- š„ **Overlay** ā Syntax-highlighted stack frames, clickable file links (`vscode://file`)
|
|
91
91
|
- š **Source Map Stack Mapping** ā Maps runtime errors to original TS/JS source lines
|
|
92
|
-
- š¬ **Auto Port Detection** ā Prompts when default port
|
|
92
|
+
- š¬ **Auto Port Detection** ā Prompts when default port 2202 is occupied
|
|
93
93
|
- š§ **Smart Config Loader** ā Detects project root, compiles `.ts` configs dynamically
|
|
94
94
|
- šØ **PrismJS Highlighting** ā For pretty overlay code frames
|
|
95
95
|
- š **Plugin Hook System** ā Extendable with `configResolved`, `transform`, `buildEnd`
|
|
@@ -142,7 +142,7 @@ npm install react-refresh
|
|
|
142
142
|
### ā ļø Port already in use
|
|
143
143
|
CLI will auto-detect and prompt:
|
|
144
144
|
```
|
|
145
|
-
Port
|
|
145
|
+
Port 2202 is occupied. Use 5174 instead? (Y/n)
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
### ā ļø Permission denied
|
package/dist/cli/commands/dev.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* š react-client Dev Server (Final Version)
|
|
4
|
+
* Includes:
|
|
5
|
+
* - Favicon & public asset support
|
|
6
|
+
* - ETag + gzip/brotli caching
|
|
7
|
+
* - Persistent prebundle deps (.react-client/deps)
|
|
8
|
+
* - HMR + overlay
|
|
9
|
+
* - CSS hot reload
|
|
10
|
+
* - ESLint + Prettier clean
|
|
11
|
+
*/
|
|
2
12
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
13
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
14
|
};
|
|
@@ -15,9 +25,11 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
|
15
25
|
const open_1 = __importDefault(require("open"));
|
|
16
26
|
const child_process_1 = require("child_process");
|
|
17
27
|
const chalk_1 = __importDefault(require("chalk"));
|
|
28
|
+
const zlib_1 = __importDefault(require("zlib"));
|
|
29
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
18
30
|
const loadConfig_1 = require("../../utils/loadConfig");
|
|
19
31
|
const broadcastManager_1 = require("../../server/broadcastManager");
|
|
20
|
-
//
|
|
32
|
+
// Node polyfill mapping
|
|
21
33
|
const NODE_POLYFILLS = {
|
|
22
34
|
buffer: 'buffer/',
|
|
23
35
|
process: 'process/browser',
|
|
@@ -34,23 +46,36 @@ const NODE_POLYFILLS = {
|
|
|
34
46
|
querystring: 'querystring-es3',
|
|
35
47
|
zlib: 'browserify-zlib',
|
|
36
48
|
};
|
|
37
|
-
//
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
// --- Helper utilities
|
|
50
|
+
const computeHash = (content) => crypto_1.default.createHash('sha1').update(content).digest('hex');
|
|
51
|
+
const getMimeType = (file) => {
|
|
52
|
+
const ext = path_1.default.extname(file).toLowerCase();
|
|
53
|
+
const mime = {
|
|
54
|
+
'.ico': 'image/x-icon',
|
|
55
|
+
'.png': 'image/png',
|
|
56
|
+
'.jpg': 'image/jpeg',
|
|
57
|
+
'.jpeg': 'image/jpeg',
|
|
58
|
+
'.gif': 'image/gif',
|
|
59
|
+
'.svg': 'image/svg+xml',
|
|
60
|
+
'.webp': 'image/webp',
|
|
61
|
+
'.json': 'application/json',
|
|
62
|
+
'.txt': 'text/plain',
|
|
63
|
+
'.js': 'application/javascript',
|
|
64
|
+
'.mjs': 'application/javascript',
|
|
65
|
+
'.css': 'text/css',
|
|
66
|
+
'.html': 'text/html',
|
|
67
|
+
'.woff': 'font/woff',
|
|
68
|
+
'.woff2': 'font/woff2',
|
|
69
|
+
'.ttf': 'font/ttf',
|
|
70
|
+
'.otf': 'font/otf',
|
|
71
|
+
'.mp4': 'video/mp4',
|
|
72
|
+
'.mp3': 'audio/mpeg',
|
|
73
|
+
};
|
|
74
|
+
return mime[ext] || 'application/octet-stream';
|
|
75
|
+
};
|
|
76
|
+
// ā
Unused helpers are underscored to comply with eslint rules
|
|
77
|
+
const _gunzipAsync = (input) => new Promise((res, rej) => zlib_1.default.gunzip(input, (e, out) => (e ? rej(e) : res(out))));
|
|
78
|
+
const _brotliAsync = (input) => new Promise((res, rej) => zlib_1.default.brotliDecompress(input, (e, out) => (e ? rej(e) : res(out))));
|
|
54
79
|
async function dev() {
|
|
55
80
|
const root = process.cwd();
|
|
56
81
|
const userConfig = await (0, loadConfig_1.loadReactClientConfig)(root);
|
|
@@ -58,66 +83,48 @@ async function dev() {
|
|
|
58
83
|
const defaultPort = userConfig.server?.port || 5173;
|
|
59
84
|
const cacheDir = path_1.default.join(appRoot, '.react-client', 'deps');
|
|
60
85
|
await fs_extra_1.default.ensureDir(cacheDir);
|
|
61
|
-
|
|
86
|
+
const indexHtml = path_1.default.join(appRoot, 'index.html');
|
|
87
|
+
const pkgFile = path_1.default.join(appRoot, 'package.json');
|
|
88
|
+
// Detect entry
|
|
62
89
|
const possibleEntries = ['src/main.tsx', 'src/main.jsx'];
|
|
63
90
|
const entry = possibleEntries.map((p) => path_1.default.join(appRoot, p)).find((p) => fs_extra_1.default.existsSync(p));
|
|
64
91
|
if (!entry) {
|
|
65
92
|
console.error(chalk_1.default.red('ā No entry found: src/main.tsx or src/main.jsx'));
|
|
66
93
|
process.exit(1);
|
|
67
94
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const port = availablePort;
|
|
72
|
-
if (availablePort !== defaultPort) {
|
|
95
|
+
// Detect open port
|
|
96
|
+
const port = await (0, detect_port_1.default)(defaultPort);
|
|
97
|
+
if (port !== defaultPort) {
|
|
73
98
|
const res = await (0, prompts_1.default)({
|
|
74
99
|
type: 'confirm',
|
|
75
100
|
name: 'useNewPort',
|
|
76
|
-
message: `Port ${defaultPort} is occupied. Use ${
|
|
101
|
+
message: `Port ${defaultPort} is occupied. Use ${port} instead?`,
|
|
77
102
|
initial: true,
|
|
78
103
|
});
|
|
79
|
-
if (!res.useNewPort)
|
|
80
|
-
console.log('š Dev server cancelled.');
|
|
104
|
+
if (!res.useNewPort)
|
|
81
105
|
process.exit(0);
|
|
82
|
-
}
|
|
83
106
|
}
|
|
84
|
-
//
|
|
107
|
+
// Ensure react-refresh installed
|
|
85
108
|
try {
|
|
86
109
|
require.resolve('react-refresh/runtime');
|
|
87
110
|
}
|
|
88
111
|
catch {
|
|
89
|
-
console.
|
|
90
|
-
(0, child_process_1.execSync)('npm
|
|
91
|
-
cwd: root,
|
|
92
|
-
stdio: 'inherit',
|
|
93
|
-
});
|
|
94
|
-
console.log(chalk_1.default.green('ā
react-refresh installed successfully.'));
|
|
112
|
+
console.log(chalk_1.default.yellow('Installing react-refresh...'));
|
|
113
|
+
(0, child_process_1.execSync)('npm i react-refresh --silent', { cwd: root, stdio: 'inherit' });
|
|
95
114
|
}
|
|
96
|
-
//
|
|
97
|
-
const
|
|
115
|
+
// Ensure Node polyfills installed
|
|
116
|
+
const missing = Object.keys(NODE_POLYFILLS).filter((m) => {
|
|
98
117
|
try {
|
|
99
|
-
require.resolve(
|
|
118
|
+
require.resolve(m, { paths: [appRoot] });
|
|
100
119
|
return false;
|
|
101
120
|
}
|
|
102
121
|
catch {
|
|
103
122
|
return true;
|
|
104
123
|
}
|
|
105
124
|
});
|
|
106
|
-
if (
|
|
107
|
-
console.log(chalk_1.default.yellow('
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
(0, child_process_1.execSync)(`npm install ${missingPolyfills.join(' ')} --no-audit --no-fund --silent`, {
|
|
111
|
-
cwd: appRoot,
|
|
112
|
-
stdio: 'inherit',
|
|
113
|
-
});
|
|
114
|
-
console.log(chalk_1.default.green('ā
Polyfills installed successfully.'));
|
|
115
|
-
}
|
|
116
|
-
catch (err) {
|
|
117
|
-
console.error(chalk_1.default.red('ā Failed to install polyfills automatically.'));
|
|
118
|
-
console.error(err);
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
125
|
+
if (missing.length > 0) {
|
|
126
|
+
console.log(chalk_1.default.yellow('Installing missing polyfills...'));
|
|
127
|
+
(0, child_process_1.execSync)(`npm i ${missing.join(' ')} --silent`, { cwd: appRoot, stdio: 'inherit' });
|
|
121
128
|
}
|
|
122
129
|
// --- Plugins
|
|
123
130
|
const corePlugins = [
|
|
@@ -141,137 +148,163 @@ async function dev() {
|
|
|
141
148
|
const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
|
|
142
149
|
const plugins = [...corePlugins, ...userPlugins];
|
|
143
150
|
const app = (0, connect_1.default)();
|
|
151
|
+
const server = http_1.default.createServer(app);
|
|
152
|
+
const broadcaster = new broadcastManager_1.BroadcastManager(server);
|
|
144
153
|
const transformCache = new Map();
|
|
145
|
-
//
|
|
146
|
-
async function
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const polyCode = result.outputFiles[0].text;
|
|
163
|
-
await fs_extra_1.default.writeFile(cacheFile, polyCode, 'utf8');
|
|
164
|
-
return polyCode;
|
|
165
|
-
}
|
|
166
|
-
// š§± Normal dependency
|
|
167
|
-
let entryPath = null;
|
|
168
|
-
try {
|
|
169
|
-
entryPath = require.resolve(id, { paths: [appRoot] });
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
const base = id.split('/')[0];
|
|
154
|
+
// --- Prebundle deps with gzip/brotli caching
|
|
155
|
+
async function prebundleDeps() {
|
|
156
|
+
if (!(await fs_extra_1.default.pathExists(pkgFile)))
|
|
157
|
+
return;
|
|
158
|
+
const pkg = JSON.parse(await fs_extra_1.default.readFile(pkgFile, 'utf8'));
|
|
159
|
+
const deps = Object.keys(pkg.dependencies || {});
|
|
160
|
+
if (!deps.length)
|
|
161
|
+
return;
|
|
162
|
+
const hash = computeHash(JSON.stringify(deps));
|
|
163
|
+
const metaFile = path_1.default.join(cacheDir, '_meta.json');
|
|
164
|
+
let prevHash = null;
|
|
165
|
+
if (await fs_extra_1.default.pathExists(metaFile))
|
|
166
|
+
prevHash = (await fs_extra_1.default.readJSON(metaFile)).hash;
|
|
167
|
+
if (prevHash === hash)
|
|
168
|
+
return;
|
|
169
|
+
console.log(chalk_1.default.cyan('š¦ Rebuilding prebundle cache...'));
|
|
170
|
+
await Promise.all(deps.map(async (dep) => {
|
|
173
171
|
try {
|
|
174
|
-
entryPath = require.resolve(
|
|
172
|
+
const entryPath = require.resolve(dep, { paths: [appRoot] });
|
|
173
|
+
const outFile = path_1.default.join(cacheDir, dep + '.js');
|
|
174
|
+
await esbuild_1.default.build({
|
|
175
|
+
entryPoints: [entryPath],
|
|
176
|
+
bundle: true,
|
|
177
|
+
platform: 'browser',
|
|
178
|
+
format: 'esm',
|
|
179
|
+
target: 'es2020',
|
|
180
|
+
outfile: outFile,
|
|
181
|
+
write: true,
|
|
182
|
+
});
|
|
183
|
+
const content = await fs_extra_1.default.readFile(outFile);
|
|
184
|
+
await fs_extra_1.default.writeFile(outFile + '.gz', zlib_1.default.gzipSync(content));
|
|
185
|
+
await fs_extra_1.default.writeFile(outFile + '.br', zlib_1.default.brotliCompressSync(content));
|
|
186
|
+
console.log(chalk_1.default.green(`ā
Cached ${dep}`));
|
|
175
187
|
}
|
|
176
|
-
catch {
|
|
177
|
-
|
|
188
|
+
catch (e) {
|
|
189
|
+
const err = e;
|
|
190
|
+
console.warn(chalk_1.default.yellow(`ā ļø Failed ${dep}: ${err.message}`));
|
|
178
191
|
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
throw new Error(`Module ${id} not found (resolve failed)`);
|
|
182
|
-
const result = await esbuild_1.default.build({
|
|
183
|
-
entryPoints: [entryPath],
|
|
184
|
-
bundle: true,
|
|
185
|
-
platform: 'browser',
|
|
186
|
-
format: 'esm',
|
|
187
|
-
target: 'es2020',
|
|
188
|
-
write: false,
|
|
189
|
-
});
|
|
190
|
-
const originalCode = result.outputFiles[0].text;
|
|
191
|
-
const isSubpath = id.includes('/');
|
|
192
|
-
let finalCode = originalCode;
|
|
193
|
-
if (isSubpath) {
|
|
194
|
-
const base = id.split('/')[0];
|
|
195
|
-
finalCode += `
|
|
196
|
-
// --- react-client auto wrapper for subpath: ${id}
|
|
197
|
-
import * as __base from '/@modules/${base}';
|
|
198
|
-
export const __rc_dynamic = __base;
|
|
199
|
-
export default __base.default || __base;
|
|
200
|
-
`;
|
|
201
|
-
}
|
|
202
|
-
await fs_extra_1.default.writeFile(cacheFile, finalCode, 'utf8');
|
|
203
|
-
return finalCode;
|
|
192
|
+
}));
|
|
193
|
+
await fs_extra_1.default.writeJSON(metaFile, { hash });
|
|
204
194
|
}
|
|
205
|
-
|
|
195
|
+
await prebundleDeps();
|
|
196
|
+
chokidar_1.default.watch(pkgFile).on('change', prebundleDeps);
|
|
197
|
+
// --- Serve /@modules/
|
|
206
198
|
app.use('/@modules/', async (req, res, next) => {
|
|
207
199
|
const id = req.url?.replace(/^\/(@modules\/)?/, '');
|
|
208
200
|
if (!id)
|
|
209
201
|
return next();
|
|
202
|
+
const base = path_1.default.join(cacheDir, id.replace(/\//g, '_') + '.js');
|
|
203
|
+
const gz = base + '.gz';
|
|
204
|
+
const br = base + '.br';
|
|
205
|
+
const accept = req.headers['accept-encoding'] || '';
|
|
210
206
|
try {
|
|
211
|
-
|
|
207
|
+
let buf = null;
|
|
208
|
+
let encoding = null;
|
|
209
|
+
if (/\bbr\b/.test(accept) && (await fs_extra_1.default.pathExists(br))) {
|
|
210
|
+
buf = await fs_extra_1.default.readFile(br);
|
|
211
|
+
encoding = 'br';
|
|
212
|
+
}
|
|
213
|
+
else if (/\bgzip\b/.test(accept) && (await fs_extra_1.default.pathExists(gz))) {
|
|
214
|
+
buf = await fs_extra_1.default.readFile(gz);
|
|
215
|
+
encoding = 'gzip';
|
|
216
|
+
}
|
|
217
|
+
else if (await fs_extra_1.default.pathExists(base)) {
|
|
218
|
+
buf = await fs_extra_1.default.readFile(base);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const entryPath = require.resolve(id, { paths: [appRoot] });
|
|
222
|
+
const result = await esbuild_1.default.build({
|
|
223
|
+
entryPoints: [entryPath],
|
|
224
|
+
bundle: true,
|
|
225
|
+
platform: 'browser',
|
|
226
|
+
format: 'esm',
|
|
227
|
+
write: false,
|
|
228
|
+
});
|
|
229
|
+
buf = Buffer.from(result.outputFiles[0].text);
|
|
230
|
+
await fs_extra_1.default.writeFile(base, buf);
|
|
231
|
+
}
|
|
232
|
+
const etag = `"${computeHash(buf)}"`;
|
|
233
|
+
if (req.headers['if-none-match'] === etag) {
|
|
234
|
+
res.writeHead(304);
|
|
235
|
+
return res.end();
|
|
236
|
+
}
|
|
212
237
|
res.setHeader('Content-Type', 'application/javascript');
|
|
213
|
-
res.
|
|
238
|
+
res.setHeader('ETag', etag);
|
|
239
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
240
|
+
if (encoding)
|
|
241
|
+
res.setHeader('Content-Encoding', encoding);
|
|
242
|
+
res.end(buf);
|
|
214
243
|
}
|
|
215
|
-
catch (
|
|
216
|
-
const
|
|
217
|
-
console.error(chalk_1.default.red(`ā Failed to load module ${id}: ${e.message}`));
|
|
244
|
+
catch (e) {
|
|
245
|
+
const err = e;
|
|
218
246
|
res.writeHead(500);
|
|
219
|
-
res.end(`// Failed to resolve module ${id}: ${
|
|
247
|
+
res.end(`// Failed to resolve module ${id}: ${err.message}`);
|
|
220
248
|
}
|
|
221
249
|
});
|
|
222
|
-
// ---
|
|
250
|
+
// --- Serve /src/ files
|
|
223
251
|
app.use(async (req, res, next) => {
|
|
224
|
-
|
|
225
|
-
if (urlPath.includes('node_modules'))
|
|
252
|
+
if (!req.url || (!req.url.startsWith('/src/') && !req.url.endsWith('.css')))
|
|
226
253
|
return next();
|
|
227
|
-
|
|
228
|
-
const possibleExts = ['', '.tsx', '.ts', '.jsx', '.js', '.css'];
|
|
229
|
-
for (const ext of possibleExts) {
|
|
230
|
-
if (await fs_extra_1.default.pathExists(filePath + ext)) {
|
|
231
|
-
filePath += ext;
|
|
232
|
-
break;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
254
|
+
const filePath = path_1.default.join(appRoot, decodeURIComponent(req.url.split('?')[0]));
|
|
235
255
|
if (!(await fs_extra_1.default.pathExists(filePath)))
|
|
236
256
|
return next();
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
257
|
+
let code = await fs_extra_1.default.readFile(filePath, 'utf8');
|
|
258
|
+
code = code
|
|
259
|
+
.replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
|
|
260
|
+
.replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
|
|
261
|
+
for (const p of plugins)
|
|
262
|
+
if (p.onTransform)
|
|
263
|
+
code = await p.onTransform(code, filePath);
|
|
264
|
+
const loader = filePath.endsWith('.tsx')
|
|
265
|
+
? 'tsx'
|
|
266
|
+
: filePath.endsWith('.ts')
|
|
267
|
+
? 'ts'
|
|
268
|
+
: filePath.endsWith('.jsx')
|
|
269
|
+
? 'jsx'
|
|
270
|
+
: 'js';
|
|
271
|
+
const result = await esbuild_1.default.transform(code, { loader, sourcemap: 'inline', target: 'es2020' });
|
|
272
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
273
|
+
res.end(result.code);
|
|
274
|
+
});
|
|
275
|
+
// --- Serve static assets (favicon, /public, etc.)
|
|
276
|
+
app.use(async (req, res, next) => {
|
|
277
|
+
if (!req.url)
|
|
278
|
+
return next();
|
|
279
|
+
const assetPath = decodeURIComponent(req.url.split('?')[0]);
|
|
280
|
+
const publicDir = path_1.default.join(appRoot, 'public');
|
|
281
|
+
const rootFile = path_1.default.join(appRoot, assetPath);
|
|
282
|
+
const publicFile = path_1.default.join(publicDir, assetPath);
|
|
283
|
+
let targetFile = null;
|
|
284
|
+
if (await fs_extra_1.default.pathExists(publicFile))
|
|
285
|
+
targetFile = publicFile;
|
|
286
|
+
else if (await fs_extra_1.default.pathExists(rootFile))
|
|
287
|
+
targetFile = rootFile;
|
|
288
|
+
if (!targetFile)
|
|
289
|
+
return next();
|
|
290
|
+
const stat = await fs_extra_1.default.stat(targetFile);
|
|
291
|
+
if (!stat.isFile())
|
|
292
|
+
return next();
|
|
293
|
+
const etag = `"${stat.size}-${stat.mtimeMs}"`;
|
|
294
|
+
if (req.headers['if-none-match'] === etag) {
|
|
295
|
+
res.writeHead(304);
|
|
296
|
+
return res.end();
|
|
268
297
|
}
|
|
298
|
+
res.setHeader('Cache-Control', 'public, max-age=3600');
|
|
299
|
+
res.setHeader('ETag', etag);
|
|
300
|
+
res.setHeader('Content-Type', getMimeType(targetFile));
|
|
301
|
+
fs_extra_1.default.createReadStream(targetFile).pipe(res);
|
|
269
302
|
});
|
|
270
|
-
// --- index.html
|
|
303
|
+
// --- Serve index.html with overlay + HMR
|
|
271
304
|
app.use(async (req, res, next) => {
|
|
272
305
|
if (req.url !== '/' && req.url !== '/index.html')
|
|
273
306
|
return next();
|
|
274
|
-
if (!fs_extra_1.default.
|
|
307
|
+
if (!(await fs_extra_1.default.pathExists(indexHtml))) {
|
|
275
308
|
res.writeHead(404);
|
|
276
309
|
return res.end('index.html not found');
|
|
277
310
|
}
|
|
@@ -282,9 +315,10 @@ async function dev() {
|
|
|
282
315
|
const style = document.createElement('style');
|
|
283
316
|
style.textContent = \`
|
|
284
317
|
.rc-overlay {
|
|
285
|
-
position: fixed; inset: 0;
|
|
286
|
-
|
|
287
|
-
padding:
|
|
318
|
+
position: fixed; inset: 0;
|
|
319
|
+
background: rgba(0,0,0,0.9); color:#fff;
|
|
320
|
+
font-family: monospace; padding:2rem; overflow:auto;
|
|
321
|
+
z-index:999999; white-space:pre-wrap;
|
|
288
322
|
}
|
|
289
323
|
\`;
|
|
290
324
|
document.head.appendChild(style);
|
|
@@ -315,28 +349,26 @@ async function dev() {
|
|
|
315
349
|
res.setHeader('Content-Type', 'text/html');
|
|
316
350
|
res.end(html);
|
|
317
351
|
});
|
|
318
|
-
// ---
|
|
319
|
-
|
|
320
|
-
const broadcaster = new broadcastManager_1.BroadcastManager(server);
|
|
321
|
-
chokidar_1.default.watch(appRoot, { ignoreInitial: true }).on('change', async (file) => {
|
|
322
|
-
if (file.includes('node_modules') || file.includes('.react-client'))
|
|
323
|
-
return;
|
|
352
|
+
// --- Watchers for HMR + favicon reload
|
|
353
|
+
chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', (file) => {
|
|
324
354
|
console.log(chalk_1.default.yellow(`š Changed: ${file}`));
|
|
325
355
|
transformCache.delete(file);
|
|
326
|
-
for (const p of plugins)
|
|
327
|
-
await p.onHotUpdate?.(file, { broadcast: (msg) => broadcaster.broadcast(msg) });
|
|
328
356
|
broadcaster.broadcast({
|
|
329
357
|
type: 'update',
|
|
330
358
|
path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/'),
|
|
331
359
|
});
|
|
332
360
|
});
|
|
361
|
+
chokidar_1.default
|
|
362
|
+
.watch(path_1.default.join(appRoot, 'public', 'favicon.ico'), { ignoreInitial: true })
|
|
363
|
+
.on('change', () => {
|
|
364
|
+
broadcaster.broadcast({ type: 'reload' });
|
|
365
|
+
});
|
|
366
|
+
// --- Start server
|
|
333
367
|
server.listen(port, async () => {
|
|
334
368
|
const url = `http://localhost:${port}`;
|
|
335
369
|
console.log(chalk_1.default.cyan.bold('\nš React Client Dev Server'));
|
|
336
370
|
console.log(chalk_1.default.gray('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
337
371
|
console.log(chalk_1.default.green(`ā” Running at: ${url}`));
|
|
338
|
-
if (port !== defaultPort)
|
|
339
|
-
console.log(chalk_1.default.yellow(`ā ļø Using alternate port (default ${defaultPort} occupied)`));
|
|
340
372
|
await (0, open_1.default)(url, { newInstance: true });
|
|
341
373
|
});
|
|
342
374
|
process.on('SIGINT', () => {
|
|
@@ -19,7 +19,7 @@ async function preview() {
|
|
|
19
19
|
const config = await (0, loadConfig_1.loadReactClientConfig)(root);
|
|
20
20
|
const appRoot = path_1.default.resolve(root, config.root || '.');
|
|
21
21
|
const outDir = path_1.default.join(appRoot, config.build?.outDir || '.react-client/build');
|
|
22
|
-
const defaultPort = config.server?.port ||
|
|
22
|
+
const defaultPort = config.server?.port || 2202;
|
|
23
23
|
if (!fs_extra_1.default.existsSync(outDir)) {
|
|
24
24
|
console.error(chalk_1.default.red(`ā Build output not found at: ${outDir}`));
|
|
25
25
|
console.log(chalk_1.default.gray('Please run `react-client build` first.'));
|