react-client 1.0.19 ā 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 +207 -159
- 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,50 +25,106 @@ 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");
|
|
32
|
+
// Node polyfill mapping
|
|
33
|
+
const NODE_POLYFILLS = {
|
|
34
|
+
buffer: 'buffer/',
|
|
35
|
+
process: 'process/browser',
|
|
36
|
+
path: 'path-browserify',
|
|
37
|
+
fs: 'browserify-fs',
|
|
38
|
+
os: 'os-browserify/browser',
|
|
39
|
+
stream: 'stream-browserify',
|
|
40
|
+
util: 'util/',
|
|
41
|
+
url: 'url/',
|
|
42
|
+
assert: 'assert/',
|
|
43
|
+
crypto: 'crypto-browserify',
|
|
44
|
+
events: 'events/',
|
|
45
|
+
constants: 'constants-browserify',
|
|
46
|
+
querystring: 'querystring-es3',
|
|
47
|
+
zlib: 'browserify-zlib',
|
|
48
|
+
};
|
|
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))));
|
|
20
79
|
async function dev() {
|
|
21
80
|
const root = process.cwd();
|
|
22
81
|
const userConfig = await (0, loadConfig_1.loadReactClientConfig)(root);
|
|
23
82
|
const appRoot = path_1.default.resolve(root, userConfig.root || '.');
|
|
24
83
|
const defaultPort = userConfig.server?.port || 5173;
|
|
25
84
|
const cacheDir = path_1.default.join(appRoot, '.react-client', 'deps');
|
|
26
|
-
const pkgFile = path_1.default.join(appRoot, 'package.json');
|
|
27
85
|
await fs_extra_1.default.ensureDir(cacheDir);
|
|
28
|
-
|
|
86
|
+
const indexHtml = path_1.default.join(appRoot, 'index.html');
|
|
87
|
+
const pkgFile = path_1.default.join(appRoot, 'package.json');
|
|
88
|
+
// Detect entry
|
|
29
89
|
const possibleEntries = ['src/main.tsx', 'src/main.jsx'];
|
|
30
90
|
const entry = possibleEntries.map((p) => path_1.default.join(appRoot, p)).find((p) => fs_extra_1.default.existsSync(p));
|
|
31
91
|
if (!entry) {
|
|
32
92
|
console.error(chalk_1.default.red('ā No entry found: src/main.tsx or src/main.jsx'));
|
|
33
93
|
process.exit(1);
|
|
34
94
|
}
|
|
35
|
-
const indexHtml = path_1.default.join(appRoot, 'index.html');
|
|
36
95
|
// Detect open port
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
if (availablePort !== defaultPort) {
|
|
96
|
+
const port = await (0, detect_port_1.default)(defaultPort);
|
|
97
|
+
if (port !== defaultPort) {
|
|
40
98
|
const res = await (0, prompts_1.default)({
|
|
41
99
|
type: 'confirm',
|
|
42
100
|
name: 'useNewPort',
|
|
43
|
-
message: `Port ${defaultPort} is occupied. Use ${
|
|
101
|
+
message: `Port ${defaultPort} is occupied. Use ${port} instead?`,
|
|
44
102
|
initial: true,
|
|
45
103
|
});
|
|
46
|
-
if (!res.useNewPort)
|
|
47
|
-
console.log('š Dev server cancelled.');
|
|
104
|
+
if (!res.useNewPort)
|
|
48
105
|
process.exit(0);
|
|
49
|
-
}
|
|
50
106
|
}
|
|
51
107
|
// Ensure react-refresh installed
|
|
52
108
|
try {
|
|
53
109
|
require.resolve('react-refresh/runtime');
|
|
54
110
|
}
|
|
55
111
|
catch {
|
|
56
|
-
console.
|
|
57
|
-
(0, child_process_1.execSync)('npm
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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' });
|
|
114
|
+
}
|
|
115
|
+
// Ensure Node polyfills installed
|
|
116
|
+
const missing = Object.keys(NODE_POLYFILLS).filter((m) => {
|
|
117
|
+
try {
|
|
118
|
+
require.resolve(m, { paths: [appRoot] });
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
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' });
|
|
62
128
|
}
|
|
63
129
|
// --- Plugins
|
|
64
130
|
const corePlugins = [
|
|
@@ -82,43 +148,26 @@ async function dev() {
|
|
|
82
148
|
const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
|
|
83
149
|
const plugins = [...corePlugins, ...userPlugins];
|
|
84
150
|
const app = (0, connect_1.default)();
|
|
151
|
+
const server = http_1.default.createServer(app);
|
|
152
|
+
const broadcaster = new broadcastManager_1.BroadcastManager(server);
|
|
85
153
|
const transformCache = new Map();
|
|
86
|
-
//
|
|
87
|
-
async function
|
|
88
|
-
if (
|
|
89
|
-
return seen;
|
|
90
|
-
seen.add(file);
|
|
91
|
-
const code = await fs_extra_1.default.readFile(file, 'utf8');
|
|
92
|
-
const matches = [
|
|
93
|
-
...code.matchAll(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g),
|
|
94
|
-
...code.matchAll(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g),
|
|
95
|
-
];
|
|
96
|
-
for (const m of matches) {
|
|
97
|
-
const dep = m[1];
|
|
98
|
-
if (!dep || dep.startsWith('.') || dep.startsWith('/'))
|
|
99
|
-
continue;
|
|
100
|
-
try {
|
|
101
|
-
const resolved = require.resolve(dep, { paths: [appRoot] });
|
|
102
|
-
await analyzeGraph(resolved, seen);
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
seen.add(dep); // bare dependency
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return seen;
|
|
109
|
-
}
|
|
110
|
-
// š¦ Smart prebundle cache (parallelized)
|
|
111
|
-
async function prebundleDeps(deps) {
|
|
112
|
-
if (!deps.size)
|
|
154
|
+
// --- Prebundle deps with gzip/brotli caching
|
|
155
|
+
async function prebundleDeps() {
|
|
156
|
+
if (!(await fs_extra_1.default.pathExists(pkgFile)))
|
|
113
157
|
return;
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
117
|
-
console.log(chalk_1.default.green('ā
All dependencies already prebundled.'));
|
|
158
|
+
const pkg = JSON.parse(await fs_extra_1.default.readFile(pkgFile, 'utf8'));
|
|
159
|
+
const deps = Object.keys(pkg.dependencies || {});
|
|
160
|
+
if (!deps.length)
|
|
118
161
|
return;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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) => {
|
|
122
171
|
try {
|
|
123
172
|
const entryPath = require.resolve(dep, { paths: [appRoot] });
|
|
124
173
|
const outFile = path_1.default.join(cacheDir, dep + '.js');
|
|
@@ -127,138 +176,135 @@ async function dev() {
|
|
|
127
176
|
bundle: true,
|
|
128
177
|
platform: 'browser',
|
|
129
178
|
format: 'esm',
|
|
179
|
+
target: 'es2020',
|
|
130
180
|
outfile: outFile,
|
|
131
181
|
write: true,
|
|
132
|
-
target: 'es2020',
|
|
133
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));
|
|
134
186
|
console.log(chalk_1.default.green(`ā
Cached ${dep}`));
|
|
135
187
|
}
|
|
136
|
-
catch (
|
|
137
|
-
const
|
|
138
|
-
console.warn(chalk_1.default.yellow(`ā ļø
|
|
188
|
+
catch (e) {
|
|
189
|
+
const err = e;
|
|
190
|
+
console.warn(chalk_1.default.yellow(`ā ļø Failed ${dep}: ${err.message}`));
|
|
139
191
|
}
|
|
140
192
|
}));
|
|
193
|
+
await fs_extra_1.default.writeJSON(metaFile, { hash });
|
|
141
194
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await prebundleDeps(deps);
|
|
145
|
-
// Auto rebuild on package.json change
|
|
146
|
-
chokidar_1.default.watch(pkgFile).on('change', async () => {
|
|
147
|
-
console.log(chalk_1.default.yellow('š¦ package.json changed ā rebuilding prebundle cache...'));
|
|
148
|
-
const newDeps = await analyzeGraph(entry);
|
|
149
|
-
await prebundleDeps(newDeps);
|
|
150
|
-
});
|
|
195
|
+
await prebundleDeps();
|
|
196
|
+
chokidar_1.default.watch(pkgFile).on('change', prebundleDeps);
|
|
151
197
|
// --- Serve /@modules/
|
|
152
198
|
app.use('/@modules/', async (req, res, next) => {
|
|
153
199
|
const id = req.url?.replace(/^\/(@modules\/)?/, '');
|
|
154
200
|
if (!id)
|
|
155
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'] || '';
|
|
156
206
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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';
|
|
161
212
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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';
|
|
165
216
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
entryPath = require.resolve('react');
|
|
169
|
-
else if (id === 'react-dom' || id === 'react-dom/client')
|
|
170
|
-
entryPath = require.resolve('react-dom');
|
|
217
|
+
else if (await fs_extra_1.default.pathExists(base)) {
|
|
218
|
+
buf = await fs_extra_1.default.readFile(base);
|
|
171
219
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (!('createRoot' in ReactDOMClient) && ReactDOMClient.default) {
|
|
189
|
-
Object.assign(ReactDOMClient, ReactDOMClient.default);
|
|
190
|
-
}
|
|
191
|
-
export const createRoot =
|
|
192
|
-
ReactDOMClient.createRoot ||
|
|
193
|
-
ReactDOMClient.default?.createRoot ||
|
|
194
|
-
(() => { throw new Error('ReactDOM.createRoot not found'); });
|
|
195
|
-
export default ReactDOMClient;
|
|
196
|
-
`;
|
|
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();
|
|
197
236
|
}
|
|
198
|
-
await fs_extra_1.default.writeFile(cacheFile, code, 'utf8');
|
|
199
237
|
res.setHeader('Content-Type', 'application/javascript');
|
|
200
|
-
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);
|
|
201
243
|
}
|
|
202
|
-
catch (
|
|
203
|
-
const
|
|
204
|
-
console.error(chalk_1.default.red(`ā Failed to load module ${id}: ${e.message}`));
|
|
244
|
+
catch (e) {
|
|
245
|
+
const err = e;
|
|
205
246
|
res.writeHead(500);
|
|
206
|
-
res.end(`// Failed to resolve module ${id}: ${
|
|
247
|
+
res.end(`// Failed to resolve module ${id}: ${err.message}`);
|
|
207
248
|
}
|
|
208
249
|
});
|
|
209
|
-
// --- Serve /src files
|
|
250
|
+
// --- Serve /src/ files
|
|
210
251
|
app.use(async (req, res, next) => {
|
|
211
252
|
if (!req.url || (!req.url.startsWith('/src/') && !req.url.endsWith('.css')))
|
|
212
253
|
return next();
|
|
213
|
-
const
|
|
214
|
-
let filePath = path_1.default.join(appRoot, rawPath);
|
|
215
|
-
const possibleExts = ['', '.tsx', '.ts', '.jsx', '.js'];
|
|
216
|
-
for (const ext of possibleExts) {
|
|
217
|
-
if (await fs_extra_1.default.pathExists(filePath + ext)) {
|
|
218
|
-
filePath += ext;
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
254
|
+
const filePath = path_1.default.join(appRoot, decodeURIComponent(req.url.split('?')[0]));
|
|
222
255
|
if (!(await fs_extra_1.default.pathExists(filePath)))
|
|
223
256
|
return next();
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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();
|
|
255
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);
|
|
256
302
|
});
|
|
257
|
-
// --- Serve index.html
|
|
303
|
+
// --- Serve index.html with overlay + HMR
|
|
258
304
|
app.use(async (req, res, next) => {
|
|
259
305
|
if (req.url !== '/' && req.url !== '/index.html')
|
|
260
306
|
return next();
|
|
261
|
-
if (!fs_extra_1.default.
|
|
307
|
+
if (!(await fs_extra_1.default.pathExists(indexHtml))) {
|
|
262
308
|
res.writeHead(404);
|
|
263
309
|
return res.end('index.html not found');
|
|
264
310
|
}
|
|
@@ -269,9 +315,10 @@ async function dev() {
|
|
|
269
315
|
const style = document.createElement('style');
|
|
270
316
|
style.textContent = \`
|
|
271
317
|
.rc-overlay {
|
|
272
|
-
position: fixed;
|
|
273
|
-
background: rgba(0,0,0,0.9); color
|
|
274
|
-
font-family: monospace; 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;
|
|
275
322
|
}
|
|
276
323
|
\`;
|
|
277
324
|
document.head.appendChild(style);
|
|
@@ -302,20 +349,21 @@ async function dev() {
|
|
|
302
349
|
res.setHeader('Content-Type', 'text/html');
|
|
303
350
|
res.end(html);
|
|
304
351
|
});
|
|
305
|
-
// ---
|
|
306
|
-
|
|
307
|
-
const broadcaster = new broadcastManager_1.BroadcastManager(server);
|
|
308
|
-
chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', async (file) => {
|
|
352
|
+
// --- Watchers for HMR + favicon reload
|
|
353
|
+
chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', (file) => {
|
|
309
354
|
console.log(chalk_1.default.yellow(`š Changed: ${file}`));
|
|
310
355
|
transformCache.delete(file);
|
|
311
|
-
for (const p of plugins) {
|
|
312
|
-
await p.onHotUpdate?.(file, { broadcast: (msg) => broadcaster.broadcast(msg) });
|
|
313
|
-
}
|
|
314
356
|
broadcaster.broadcast({
|
|
315
357
|
type: 'update',
|
|
316
358
|
path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/'),
|
|
317
359
|
});
|
|
318
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
|
|
319
367
|
server.listen(port, async () => {
|
|
320
368
|
const url = `http://localhost:${port}`;
|
|
321
369
|
console.log(chalk_1.default.cyan.bold('\nš React Client Dev Server'));
|
|
@@ -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.'));
|