react-client 1.0.21 ā 1.0.23
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 +137 -233
- package/dist/cli/commands/preview.js +134 -27
- package/package.json +1 -1
package/dist/cli/commands/dev.js
CHANGED
|
@@ -1,14 +1,4 @@
|
|
|
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
|
-
*/
|
|
12
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
4
|
};
|
|
@@ -25,108 +15,52 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
|
25
15
|
const open_1 = __importDefault(require("open"));
|
|
26
16
|
const child_process_1 = require("child_process");
|
|
27
17
|
const chalk_1 = __importDefault(require("chalk"));
|
|
28
|
-
const zlib_1 = __importDefault(require("zlib"));
|
|
29
|
-
const crypto_1 = __importDefault(require("crypto"));
|
|
30
18
|
const loadConfig_1 = require("../../utils/loadConfig");
|
|
31
19
|
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))));
|
|
79
20
|
async function dev() {
|
|
80
21
|
const root = process.cwd();
|
|
81
22
|
const userConfig = await (0, loadConfig_1.loadReactClientConfig)(root);
|
|
82
23
|
const appRoot = path_1.default.resolve(root, userConfig.root || '.');
|
|
83
24
|
const defaultPort = userConfig.server?.port || 5173;
|
|
84
25
|
const cacheDir = path_1.default.join(appRoot, '.react-client', 'deps');
|
|
85
|
-
await fs_extra_1.default.ensureDir(cacheDir);
|
|
86
|
-
const indexHtml = path_1.default.join(appRoot, 'index.html');
|
|
87
26
|
const pkgFile = path_1.default.join(appRoot, 'package.json');
|
|
88
|
-
|
|
27
|
+
await fs_extra_1.default.ensureDir(cacheDir);
|
|
28
|
+
// ā
Detect entry (main.tsx or main.jsx)
|
|
89
29
|
const possibleEntries = ['src/main.tsx', 'src/main.jsx'];
|
|
90
30
|
const entry = possibleEntries.map((p) => path_1.default.join(appRoot, p)).find((p) => fs_extra_1.default.existsSync(p));
|
|
91
31
|
if (!entry) {
|
|
92
32
|
console.error(chalk_1.default.red('ā No entry found: src/main.tsx or src/main.jsx'));
|
|
93
33
|
process.exit(1);
|
|
94
34
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
35
|
+
const indexHtml = path_1.default.join(appRoot, 'index.html');
|
|
36
|
+
// ā
Detect open port
|
|
37
|
+
const availablePort = await (0, detect_port_1.default)(defaultPort);
|
|
38
|
+
const port = availablePort;
|
|
39
|
+
if (availablePort !== defaultPort) {
|
|
98
40
|
const res = await (0, prompts_1.default)({
|
|
99
41
|
type: 'confirm',
|
|
100
42
|
name: 'useNewPort',
|
|
101
|
-
message: `Port ${defaultPort} is occupied. Use ${
|
|
43
|
+
message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
|
|
102
44
|
initial: true,
|
|
103
45
|
});
|
|
104
|
-
if (!res.useNewPort)
|
|
46
|
+
if (!res.useNewPort) {
|
|
47
|
+
console.log('š Dev server cancelled.');
|
|
105
48
|
process.exit(0);
|
|
49
|
+
}
|
|
106
50
|
}
|
|
107
|
-
// Ensure react-refresh installed
|
|
51
|
+
// ā
Ensure react-refresh installed
|
|
108
52
|
try {
|
|
109
53
|
require.resolve('react-refresh/runtime');
|
|
110
54
|
}
|
|
111
55
|
catch {
|
|
112
|
-
console.
|
|
113
|
-
(0, child_process_1.execSync)('npm
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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' });
|
|
56
|
+
console.warn(chalk_1.default.yellow('ā ļø react-refresh not found ā installing...'));
|
|
57
|
+
(0, child_process_1.execSync)('npm install react-refresh --no-audit --no-fund --silent', {
|
|
58
|
+
cwd: root,
|
|
59
|
+
stdio: 'inherit',
|
|
60
|
+
});
|
|
61
|
+
console.log(chalk_1.default.green('ā
react-refresh installed successfully.'));
|
|
128
62
|
}
|
|
129
|
-
//
|
|
63
|
+
// ā
Core + User Plugins
|
|
130
64
|
const corePlugins = [
|
|
131
65
|
{
|
|
132
66
|
name: 'css-hmr',
|
|
@@ -148,26 +82,43 @@ async function dev() {
|
|
|
148
82
|
const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
|
|
149
83
|
const plugins = [...corePlugins, ...userPlugins];
|
|
150
84
|
const app = (0, connect_1.default)();
|
|
151
|
-
const server = http_1.default.createServer(app);
|
|
152
|
-
const broadcaster = new broadcastManager_1.BroadcastManager(server);
|
|
153
85
|
const transformCache = new Map();
|
|
154
|
-
//
|
|
155
|
-
async function
|
|
156
|
-
if (
|
|
157
|
-
return;
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
86
|
+
// ā
Analyze dependency graph recursively
|
|
87
|
+
async function analyzeGraph(file, seen = new Set()) {
|
|
88
|
+
if (seen.has(file))
|
|
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);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return seen;
|
|
109
|
+
}
|
|
110
|
+
// ā
Smart prebundling cache
|
|
111
|
+
async function prebundleDeps(deps) {
|
|
112
|
+
if (!deps.size)
|
|
161
113
|
return;
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
prevHash = (await fs_extra_1.default.readJSON(metaFile)).hash;
|
|
167
|
-
if (prevHash === hash)
|
|
114
|
+
const cached = (await fs_extra_1.default.readdir(cacheDir)).map((f) => f.replace('.js', ''));
|
|
115
|
+
const missing = [...deps].filter((d) => !cached.includes(d));
|
|
116
|
+
if (!missing.length) {
|
|
117
|
+
console.log(chalk_1.default.green('ā
All dependencies already prebundled.'));
|
|
168
118
|
return;
|
|
169
|
-
|
|
170
|
-
|
|
119
|
+
}
|
|
120
|
+
console.log(chalk_1.default.cyan('š¦ Prebundling:'), missing.join(', '));
|
|
121
|
+
await Promise.all(missing.map(async (dep) => {
|
|
171
122
|
try {
|
|
172
123
|
const entryPath = require.resolve(dep, { paths: [appRoot] });
|
|
173
124
|
const outFile = path_1.default.join(cacheDir, dep + '.js');
|
|
@@ -176,163 +127,118 @@ async function dev() {
|
|
|
176
127
|
bundle: true,
|
|
177
128
|
platform: 'browser',
|
|
178
129
|
format: 'esm',
|
|
179
|
-
target: 'es2020',
|
|
180
130
|
outfile: outFile,
|
|
181
131
|
write: true,
|
|
132
|
+
target: 'es2020',
|
|
182
133
|
});
|
|
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
134
|
console.log(chalk_1.default.green(`ā
Cached ${dep}`));
|
|
187
135
|
}
|
|
188
|
-
catch (
|
|
189
|
-
const
|
|
190
|
-
console.warn(chalk_1.default.yellow(`ā ļø
|
|
136
|
+
catch (err) {
|
|
137
|
+
const e = err;
|
|
138
|
+
console.warn(chalk_1.default.yellow(`ā ļø Skipped ${dep}: ${e.message}`));
|
|
191
139
|
}
|
|
192
140
|
}));
|
|
193
|
-
await fs_extra_1.default.writeJSON(metaFile, { hash });
|
|
194
141
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
142
|
+
// ā
Initial dependency prebundle
|
|
143
|
+
const deps = await analyzeGraph(entry);
|
|
144
|
+
await prebundleDeps(deps);
|
|
145
|
+
// ā
Auto re-prebundle when package.json changes
|
|
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
|
+
});
|
|
151
|
+
// ā
Serve /@modules/
|
|
198
152
|
app.use('/@modules/', async (req, res, next) => {
|
|
199
153
|
const id = req.url?.replace(/^\/(@modules\/)?/, '');
|
|
200
154
|
if (!id)
|
|
201
155
|
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'] || '';
|
|
206
156
|
try {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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();
|
|
157
|
+
const cacheFile = path_1.default.join(cacheDir, id.replace(/\//g, '_') + '.js');
|
|
158
|
+
if (await fs_extra_1.default.pathExists(cacheFile)) {
|
|
159
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
160
|
+
return res.end(await fs_extra_1.default.readFile(cacheFile));
|
|
236
161
|
}
|
|
162
|
+
const entryPath = require.resolve(id, { paths: [appRoot] });
|
|
163
|
+
const result = await esbuild_1.default.build({
|
|
164
|
+
entryPoints: [entryPath],
|
|
165
|
+
bundle: true,
|
|
166
|
+
platform: 'browser',
|
|
167
|
+
format: 'esm',
|
|
168
|
+
target: 'es2020',
|
|
169
|
+
write: false,
|
|
170
|
+
});
|
|
171
|
+
const code = result.outputFiles[0].text;
|
|
172
|
+
await fs_extra_1.default.writeFile(cacheFile, code, 'utf8');
|
|
237
173
|
res.setHeader('Content-Type', 'application/javascript');
|
|
238
|
-
res.
|
|
239
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
240
|
-
if (encoding)
|
|
241
|
-
res.setHeader('Content-Encoding', encoding);
|
|
242
|
-
res.end(buf);
|
|
174
|
+
res.end(code);
|
|
243
175
|
}
|
|
244
|
-
catch (
|
|
245
|
-
const
|
|
176
|
+
catch (err) {
|
|
177
|
+
const e = err;
|
|
178
|
+
console.error(chalk_1.default.red(`ā Failed to load module ${id}: ${e.message}`));
|
|
246
179
|
res.writeHead(500);
|
|
247
|
-
res.end(`// Failed to resolve module ${id}: ${
|
|
180
|
+
res.end(`// Failed to resolve module ${id}: ${e.message}`);
|
|
248
181
|
}
|
|
249
182
|
});
|
|
250
|
-
//
|
|
183
|
+
// ā
Serve /src files dynamically
|
|
251
184
|
app.use(async (req, res, next) => {
|
|
252
185
|
if (!req.url || (!req.url.startsWith('/src/') && !req.url.endsWith('.css')))
|
|
253
186
|
return next();
|
|
254
|
-
const
|
|
187
|
+
const rawPath = decodeURIComponent(req.url.split('?')[0]);
|
|
188
|
+
let filePath = path_1.default.join(appRoot, rawPath);
|
|
189
|
+
const possibleExts = ['', '.tsx', '.ts', '.jsx', '.js'];
|
|
190
|
+
for (const ext of possibleExts) {
|
|
191
|
+
if (await fs_extra_1.default.pathExists(filePath + ext)) {
|
|
192
|
+
filePath += ext;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
255
196
|
if (!(await fs_extra_1.default.pathExists(filePath)))
|
|
256
197
|
return next();
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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();
|
|
198
|
+
try {
|
|
199
|
+
let code = await fs_extra_1.default.readFile(filePath, 'utf8');
|
|
200
|
+
// Rewrite bare imports ā /@modules/*
|
|
201
|
+
code = code
|
|
202
|
+
.replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
|
|
203
|
+
.replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
|
|
204
|
+
for (const p of plugins)
|
|
205
|
+
if (p.onTransform)
|
|
206
|
+
code = await p.onTransform(code, filePath);
|
|
207
|
+
const ext = path_1.default.extname(filePath);
|
|
208
|
+
let loader = 'js';
|
|
209
|
+
if (ext === '.ts')
|
|
210
|
+
loader = 'ts';
|
|
211
|
+
else if (ext === '.tsx')
|
|
212
|
+
loader = 'tsx';
|
|
213
|
+
else if (ext === '.jsx')
|
|
214
|
+
loader = 'jsx';
|
|
215
|
+
const result = await esbuild_1.default.transform(code, {
|
|
216
|
+
loader,
|
|
217
|
+
sourcemap: 'inline',
|
|
218
|
+
target: 'es2020',
|
|
219
|
+
});
|
|
220
|
+
transformCache.set(filePath, result.code);
|
|
221
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
222
|
+
res.end(result.code);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
const e = err;
|
|
226
|
+
console.error(chalk_1.default.red(`ā ļø Transform failed: ${e.message}`));
|
|
227
|
+
res.writeHead(500);
|
|
228
|
+
res.end(`// Error: ${e.message}`);
|
|
297
229
|
}
|
|
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);
|
|
302
230
|
});
|
|
303
|
-
//
|
|
231
|
+
// ā
Serve index.html + overlay + runtime
|
|
304
232
|
app.use(async (req, res, next) => {
|
|
305
233
|
if (req.url !== '/' && req.url !== '/index.html')
|
|
306
234
|
return next();
|
|
307
|
-
if (!
|
|
235
|
+
if (!fs_extra_1.default.existsSync(indexHtml)) {
|
|
308
236
|
res.writeHead(404);
|
|
309
237
|
return res.end('index.html not found');
|
|
310
238
|
}
|
|
311
239
|
let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
|
|
312
240
|
html = html.replace('</body>', `
|
|
313
|
-
<script>
|
|
314
|
-
(() => {
|
|
315
|
-
const style = document.createElement('style');
|
|
316
|
-
style.textContent = \`
|
|
317
|
-
.rc-overlay {
|
|
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;
|
|
322
|
-
}
|
|
323
|
-
\`;
|
|
324
|
-
document.head.appendChild(style);
|
|
325
|
-
window.showErrorOverlay = (err) => {
|
|
326
|
-
window.clearErrorOverlay?.();
|
|
327
|
-
const el = document.createElement('div');
|
|
328
|
-
el.className = 'rc-overlay';
|
|
329
|
-
el.innerHTML = '<h2>šØ Error</h2><pre>' + (err.message || err) + '</pre>';
|
|
330
|
-
document.body.appendChild(el);
|
|
331
|
-
window.__overlay = el;
|
|
332
|
-
};
|
|
333
|
-
window.clearErrorOverlay = () => window.__overlay?.remove();
|
|
334
|
-
})();
|
|
335
|
-
</script>
|
|
241
|
+
<script type="module" src="/src/runtime/overlay-runtime.js"></script>
|
|
336
242
|
<script type="module">
|
|
337
243
|
const ws = new WebSocket("ws://" + location.host);
|
|
338
244
|
ws.onmessage = (e) => {
|
|
@@ -349,21 +255,19 @@ async function dev() {
|
|
|
349
255
|
res.setHeader('Content-Type', 'text/html');
|
|
350
256
|
res.end(html);
|
|
351
257
|
});
|
|
352
|
-
//
|
|
353
|
-
|
|
258
|
+
// ā
WebSocket + HMR
|
|
259
|
+
const server = http_1.default.createServer(app);
|
|
260
|
+
const broadcaster = new broadcastManager_1.BroadcastManager(server);
|
|
261
|
+
chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', async (file) => {
|
|
354
262
|
console.log(chalk_1.default.yellow(`š Changed: ${file}`));
|
|
355
263
|
transformCache.delete(file);
|
|
264
|
+
for (const p of plugins)
|
|
265
|
+
await p.onHotUpdate?.(file, { broadcast: (msg) => broadcaster.broadcast(msg) });
|
|
356
266
|
broadcaster.broadcast({
|
|
357
267
|
type: 'update',
|
|
358
268
|
path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/'),
|
|
359
269
|
});
|
|
360
270
|
});
|
|
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
|
|
367
271
|
server.listen(port, async () => {
|
|
368
272
|
const url = `http://localhost:${port}`;
|
|
369
273
|
console.log(chalk_1.default.cyan.bold('\nš React Client Dev Server'));
|
|
@@ -4,54 +4,161 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.default = preview;
|
|
7
|
-
const connect_1 = __importDefault(require("connect"));
|
|
8
|
-
const serve_static_1 = __importDefault(require("serve-static"));
|
|
9
7
|
const http_1 = __importDefault(require("http"));
|
|
10
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
11
|
const detect_port_1 = __importDefault(require("detect-port"));
|
|
12
12
|
const prompts_1 = __importDefault(require("prompts"));
|
|
13
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
14
13
|
const open_1 = __importDefault(require("open"));
|
|
15
|
-
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
16
14
|
const loadConfig_1 = require("../../utils/loadConfig");
|
|
15
|
+
const MIME = {
|
|
16
|
+
'.html': 'text/html; charset=utf-8',
|
|
17
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
18
|
+
'.mjs': 'application/javascript; charset=utf-8',
|
|
19
|
+
'.json': 'application/json; charset=utf-8',
|
|
20
|
+
'.css': 'text/css; charset=utf-8',
|
|
21
|
+
'.png': 'image/png',
|
|
22
|
+
'.jpg': 'image/jpeg',
|
|
23
|
+
'.jpeg': 'image/jpeg',
|
|
24
|
+
'.gif': 'image/gif',
|
|
25
|
+
'.svg': 'image/svg+xml',
|
|
26
|
+
'.ico': 'image/x-icon',
|
|
27
|
+
'.woff': 'font/woff',
|
|
28
|
+
'.woff2': 'font/woff2',
|
|
29
|
+
'.ttf': 'font/ttf',
|
|
30
|
+
'.map': 'application/octet-stream',
|
|
31
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
32
|
+
};
|
|
33
|
+
function contentType(file) {
|
|
34
|
+
return MIME[path_1.default.extname(file).toLowerCase()] || 'application/octet-stream';
|
|
35
|
+
}
|
|
36
|
+
function setCachingHeaders(res, stat) {
|
|
37
|
+
// Short cache for preview by default, but set ETag/Last-Modified so browsers behave nicely
|
|
38
|
+
const etag = `${stat.size}-${Date.parse(stat.mtime.toString())}`;
|
|
39
|
+
res.setHeader('ETag', etag);
|
|
40
|
+
res.setHeader('Last-Modified', stat.mtime.toUTCString());
|
|
41
|
+
res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate');
|
|
42
|
+
}
|
|
17
43
|
async function preview() {
|
|
18
|
-
const
|
|
19
|
-
const config = await (0, loadConfig_1.loadReactClientConfig)(
|
|
20
|
-
const appRoot = path_1.default.resolve(
|
|
21
|
-
const outDir = path_1.default.join(appRoot, config.build?.outDir || '
|
|
22
|
-
const
|
|
23
|
-
if (!fs_extra_1.default.
|
|
24
|
-
console.error(chalk_1.default.red(`ā
|
|
25
|
-
console.log(chalk_1.default.gray('Please run `react-client build` first.'));
|
|
44
|
+
const cwd = process.cwd();
|
|
45
|
+
const config = await (0, loadConfig_1.loadReactClientConfig)(cwd);
|
|
46
|
+
const appRoot = path_1.default.resolve(cwd, config.root || '.');
|
|
47
|
+
const outDir = path_1.default.join(appRoot, config.build?.outDir || 'dist');
|
|
48
|
+
const indexHtml = path_1.default.join(outDir, 'index.html');
|
|
49
|
+
if (!(await fs_extra_1.default.pathExists(outDir))) {
|
|
50
|
+
console.error(chalk_1.default.red(`ā Preview directory not found: ${outDir}`));
|
|
26
51
|
process.exit(1);
|
|
27
52
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
53
|
+
if (!(await fs_extra_1.default.pathExists(indexHtml))) {
|
|
54
|
+
console.warn(chalk_1.default.yellow(`ā ļø index.html not found in ${outDir}. SPA fallback will be disabled.`));
|
|
55
|
+
}
|
|
56
|
+
const defaultPort = config.server?.port || 4173;
|
|
57
|
+
const port = await (0, detect_port_1.default)(defaultPort);
|
|
58
|
+
if (port !== defaultPort) {
|
|
59
|
+
const r = await (0, prompts_1.default)({
|
|
32
60
|
type: 'confirm',
|
|
33
61
|
name: 'useNewPort',
|
|
34
|
-
message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
|
|
35
62
|
initial: true,
|
|
63
|
+
message: `Port ${defaultPort} is occupied. Use ${port} instead?`,
|
|
36
64
|
});
|
|
37
|
-
if (!
|
|
38
|
-
console.log(
|
|
65
|
+
if (!r.useNewPort) {
|
|
66
|
+
console.log('š Preview cancelled.');
|
|
39
67
|
process.exit(0);
|
|
40
68
|
}
|
|
41
69
|
}
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
70
|
+
const server = http_1.default.createServer(async (req, res) => {
|
|
71
|
+
try {
|
|
72
|
+
const url = req.url || '/';
|
|
73
|
+
// normalize and protect
|
|
74
|
+
const relPath = decodeURIComponent(url.split('?')[0]);
|
|
75
|
+
if (relPath.includes('..')) {
|
|
76
|
+
res.writeHead(400);
|
|
77
|
+
return res.end('Invalid request');
|
|
78
|
+
}
|
|
79
|
+
// handle root -> index.html
|
|
80
|
+
let filePath = path_1.default.join(outDir, relPath);
|
|
81
|
+
const tryIndexFallback = async () => {
|
|
82
|
+
if (await fs_extra_1.default.pathExists(indexHtml)) {
|
|
83
|
+
const stat = await fs_extra_1.default.stat(indexHtml);
|
|
84
|
+
setCachingHeaders(res, stat);
|
|
85
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
86
|
+
return fs_extra_1.default.createReadStream(indexHtml).pipe(res);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
res.writeHead(404);
|
|
90
|
+
return res.end('Not found');
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
// If the request path is a directory, try index.html inside it
|
|
94
|
+
if (relPath.endsWith('/')) {
|
|
95
|
+
const candidate = path_1.default.join(filePath, 'index.html');
|
|
96
|
+
if (await fs_extra_1.default.pathExists(candidate)) {
|
|
97
|
+
filePath = candidate;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
return tryIndexFallback();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// If file doesn't exist, fallback to index.html for SPA routes
|
|
104
|
+
if (!(await fs_extra_1.default.pathExists(filePath))) {
|
|
105
|
+
// If request appears to be a static asset (has extension), return 404
|
|
106
|
+
if (path_1.default.extname(filePath)) {
|
|
107
|
+
res.writeHead(404);
|
|
108
|
+
return res.end('Not found');
|
|
109
|
+
}
|
|
110
|
+
return tryIndexFallback();
|
|
111
|
+
}
|
|
112
|
+
const stat = await fs_extra_1.default.stat(filePath);
|
|
113
|
+
if (!stat.isFile()) {
|
|
114
|
+
return tryIndexFallback();
|
|
115
|
+
}
|
|
116
|
+
// Compression/Precompressed support: prefer brotli -> gzip -> raw
|
|
117
|
+
const accept = (req.headers['accept-encoding'] || '');
|
|
118
|
+
const tryPrecompressed = async () => {
|
|
119
|
+
if (accept.includes('br') && (await fs_extra_1.default.pathExists(filePath + '.br'))) {
|
|
120
|
+
res.setHeader('Content-Encoding', 'br');
|
|
121
|
+
res.setHeader('Content-Type', contentType(filePath));
|
|
122
|
+
setCachingHeaders(res, stat);
|
|
123
|
+
return fs_extra_1.default.createReadStream(filePath + '.br').pipe(res);
|
|
124
|
+
}
|
|
125
|
+
if (accept.includes('gzip') && (await fs_extra_1.default.pathExists(filePath + '.gz'))) {
|
|
126
|
+
res.setHeader('Content-Encoding', 'gzip');
|
|
127
|
+
res.setHeader('Content-Type', contentType(filePath));
|
|
128
|
+
setCachingHeaders(res, stat);
|
|
129
|
+
return fs_extra_1.default.createReadStream(filePath + '.gz').pipe(res);
|
|
130
|
+
}
|
|
131
|
+
// default
|
|
132
|
+
res.setHeader('Content-Type', contentType(filePath));
|
|
133
|
+
setCachingHeaders(res, stat);
|
|
134
|
+
return fs_extra_1.default.createReadStream(filePath).pipe(res);
|
|
135
|
+
};
|
|
136
|
+
// ETag / If-None-Match handling
|
|
137
|
+
const etag = `${stat.size}-${Date.parse(stat.mtime.toString())}`;
|
|
138
|
+
const inm = req.headers['if-none-match'];
|
|
139
|
+
if (inm && inm.toString() === etag) {
|
|
140
|
+
res.writeHead(304);
|
|
141
|
+
return res.end();
|
|
142
|
+
}
|
|
143
|
+
return tryPrecompressed();
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
const e = err;
|
|
147
|
+
console.error('Preview server error:', e);
|
|
148
|
+
res.writeHead(500);
|
|
149
|
+
res.end('Internal Server Error');
|
|
150
|
+
}
|
|
151
|
+
});
|
|
45
152
|
server.listen(port, async () => {
|
|
46
153
|
const url = `http://localhost:${port}`;
|
|
47
|
-
console.log(chalk_1.default.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
154
|
+
console.log(chalk_1.default.cyan.bold('\nš react-client preview'));
|
|
155
|
+
console.log(chalk_1.default.gray('āāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
156
|
+
console.log(chalk_1.default.green(`Serving: ${outDir}`));
|
|
157
|
+
console.log(chalk_1.default.green(`Open: ${url}`));
|
|
51
158
|
await (0, open_1.default)(url, { newInstance: true });
|
|
52
159
|
});
|
|
53
160
|
process.on('SIGINT', () => {
|
|
54
|
-
console.log(chalk_1.default.red('\nš Shutting down preview
|
|
161
|
+
console.log(chalk_1.default.red('\nš Shutting down preview...'));
|
|
55
162
|
server.close();
|
|
56
163
|
process.exit(0);
|
|
57
164
|
});
|