react-client 1.0.21 ā 1.0.22
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 +117 -161
- package/dist/cli/commands/preview.js +134 -27
- package/package.json +1 -1
package/dist/cli/commands/dev.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* š
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
3
|
+
* š React Client Dev Server ā Final Version
|
|
4
|
+
* ------------------------------------------
|
|
5
|
+
* ā
Local overlay-runtime.js (Prism + stack mapping)
|
|
6
|
+
* ā
Dynamic /@runtime/overlay-runtime.js alias
|
|
7
|
+
* ā
Automatic HTML injection for overlay + HMR
|
|
8
|
+
* ā
Prebundle cache (.react-client/deps)
|
|
9
|
+
* ā
CSS HMR, relative & bare import handling
|
|
10
|
+
* ā
Favicon & public assets serving
|
|
11
11
|
*/
|
|
12
12
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
13
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -29,24 +29,6 @@ const zlib_1 = __importDefault(require("zlib"));
|
|
|
29
29
|
const crypto_1 = __importDefault(require("crypto"));
|
|
30
30
|
const loadConfig_1 = require("../../utils/loadConfig");
|
|
31
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
32
|
const computeHash = (content) => crypto_1.default.createHash('sha1').update(content).digest('hex');
|
|
51
33
|
const getMimeType = (file) => {
|
|
52
34
|
const ext = path_1.default.extname(file).toLowerCase();
|
|
@@ -64,35 +46,26 @@ const getMimeType = (file) => {
|
|
|
64
46
|
'.mjs': 'application/javascript',
|
|
65
47
|
'.css': 'text/css',
|
|
66
48
|
'.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
49
|
};
|
|
74
50
|
return mime[ext] || 'application/octet-stream';
|
|
75
51
|
};
|
|
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
52
|
async function dev() {
|
|
80
53
|
const root = process.cwd();
|
|
81
54
|
const userConfig = await (0, loadConfig_1.loadReactClientConfig)(root);
|
|
82
55
|
const appRoot = path_1.default.resolve(root, userConfig.root || '.');
|
|
83
56
|
const defaultPort = userConfig.server?.port || 5173;
|
|
84
57
|
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
58
|
const pkgFile = path_1.default.join(appRoot, 'package.json');
|
|
88
|
-
|
|
59
|
+
const indexHtml = path_1.default.join(appRoot, 'index.html');
|
|
60
|
+
await fs_extra_1.default.ensureDir(cacheDir);
|
|
61
|
+
// ā
Detect entry
|
|
89
62
|
const possibleEntries = ['src/main.tsx', 'src/main.jsx'];
|
|
90
63
|
const entry = possibleEntries.map((p) => path_1.default.join(appRoot, p)).find((p) => fs_extra_1.default.existsSync(p));
|
|
91
64
|
if (!entry) {
|
|
92
65
|
console.error(chalk_1.default.red('ā No entry found: src/main.tsx or src/main.jsx'));
|
|
93
66
|
process.exit(1);
|
|
94
67
|
}
|
|
95
|
-
// Detect
|
|
68
|
+
// ā
Detect free port
|
|
96
69
|
const port = await (0, detect_port_1.default)(defaultPort);
|
|
97
70
|
if (port !== defaultPort) {
|
|
98
71
|
const res = await (0, prompts_1.default)({
|
|
@@ -104,7 +77,7 @@ async function dev() {
|
|
|
104
77
|
if (!res.useNewPort)
|
|
105
78
|
process.exit(0);
|
|
106
79
|
}
|
|
107
|
-
// Ensure react-refresh
|
|
80
|
+
// ā
Ensure react-refresh
|
|
108
81
|
try {
|
|
109
82
|
require.resolve('react-refresh/runtime');
|
|
110
83
|
}
|
|
@@ -112,21 +85,7 @@ async function dev() {
|
|
|
112
85
|
console.log(chalk_1.default.yellow('Installing react-refresh...'));
|
|
113
86
|
(0, child_process_1.execSync)('npm i react-refresh --silent', { cwd: root, stdio: 'inherit' });
|
|
114
87
|
}
|
|
115
|
-
//
|
|
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' });
|
|
128
|
-
}
|
|
129
|
-
// --- Plugins
|
|
88
|
+
// ā
Core + user plugins
|
|
130
89
|
const corePlugins = [
|
|
131
90
|
{
|
|
132
91
|
name: 'css-hmr',
|
|
@@ -151,7 +110,7 @@ async function dev() {
|
|
|
151
110
|
const server = http_1.default.createServer(app);
|
|
152
111
|
const broadcaster = new broadcastManager_1.BroadcastManager(server);
|
|
153
112
|
const transformCache = new Map();
|
|
154
|
-
//
|
|
113
|
+
// š§± Persistent prebundle cache
|
|
155
114
|
async function prebundleDeps() {
|
|
156
115
|
if (!(await fs_extra_1.default.pathExists(pkgFile)))
|
|
157
116
|
return;
|
|
@@ -187,120 +146,139 @@ async function dev() {
|
|
|
187
146
|
}
|
|
188
147
|
catch (e) {
|
|
189
148
|
const err = e;
|
|
190
|
-
console.warn(chalk_1.default.yellow(`ā ļø
|
|
149
|
+
console.warn(chalk_1.default.yellow(`ā ļø Skipped ${dep}: ${err.message}`));
|
|
191
150
|
}
|
|
192
151
|
}));
|
|
193
152
|
await fs_extra_1.default.writeJSON(metaFile, { hash });
|
|
194
153
|
}
|
|
195
154
|
await prebundleDeps();
|
|
196
155
|
chokidar_1.default.watch(pkgFile).on('change', prebundleDeps);
|
|
197
|
-
//
|
|
156
|
+
// š§© Serve local overlay runtime
|
|
157
|
+
app.use('/@runtime/overlay-runtime.js', async (req, res) => {
|
|
158
|
+
const overlayPath = path_1.default.join(appRoot, 'src/runtime/overlay-runtime.js');
|
|
159
|
+
try {
|
|
160
|
+
if (!(await fs_extra_1.default.pathExists(overlayPath))) {
|
|
161
|
+
res.writeHead(404);
|
|
162
|
+
return res.end(`// Overlay runtime not found: ${overlayPath}`);
|
|
163
|
+
}
|
|
164
|
+
let code = await fs_extra_1.default.readFile(overlayPath, 'utf8');
|
|
165
|
+
// Transform bare imports ā /@modules/*
|
|
166
|
+
code = code
|
|
167
|
+
.replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
|
|
168
|
+
.replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
|
|
169
|
+
const result = await esbuild_1.default.transform(code, {
|
|
170
|
+
loader: 'js',
|
|
171
|
+
sourcemap: 'inline',
|
|
172
|
+
target: 'es2020',
|
|
173
|
+
});
|
|
174
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
175
|
+
res.end(result.code);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
const e = err;
|
|
179
|
+
console.error(chalk_1.default.red(`ā Failed to load overlay runtime: ${e.message}`));
|
|
180
|
+
res.writeHead(500);
|
|
181
|
+
res.end(`// Failed to load overlay runtime: ${e.message}`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// š§ Serve /@modules/
|
|
198
185
|
app.use('/@modules/', async (req, res, next) => {
|
|
199
186
|
const id = req.url?.replace(/^\/(@modules\/)?/, '');
|
|
200
187
|
if (!id)
|
|
201
188
|
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
189
|
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();
|
|
190
|
+
const cacheFile = path_1.default.join(cacheDir, id.replace(/\//g, '_') + '.js');
|
|
191
|
+
if (await fs_extra_1.default.pathExists(cacheFile)) {
|
|
192
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
193
|
+
return res.end(await fs_extra_1.default.readFile(cacheFile));
|
|
236
194
|
}
|
|
195
|
+
const entryPath = require.resolve(id, { paths: [appRoot] });
|
|
196
|
+
const result = await esbuild_1.default.build({
|
|
197
|
+
entryPoints: [entryPath],
|
|
198
|
+
bundle: true,
|
|
199
|
+
platform: 'browser',
|
|
200
|
+
format: 'esm',
|
|
201
|
+
target: 'es2020',
|
|
202
|
+
write: false,
|
|
203
|
+
});
|
|
204
|
+
const code = result.outputFiles[0].text;
|
|
205
|
+
await fs_extra_1.default.writeFile(cacheFile, code, 'utf8');
|
|
237
206
|
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);
|
|
207
|
+
res.end(code);
|
|
243
208
|
}
|
|
244
209
|
catch (e) {
|
|
245
210
|
const err = e;
|
|
211
|
+
console.error(chalk_1.default.red(`ā Failed to load module ${id}: ${err.message}`));
|
|
246
212
|
res.writeHead(500);
|
|
247
213
|
res.end(`// Failed to resolve module ${id}: ${err.message}`);
|
|
248
214
|
}
|
|
249
215
|
});
|
|
250
|
-
//
|
|
216
|
+
// š§© Serve /src/ and .css files dynamically
|
|
251
217
|
app.use(async (req, res, next) => {
|
|
252
218
|
if (!req.url || (!req.url.startsWith('/src/') && !req.url.endsWith('.css')))
|
|
253
219
|
return next();
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
let
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
220
|
+
const rawPath = decodeURIComponent(req.url.split('?')[0]);
|
|
221
|
+
const filePath = path_1.default.join(appRoot, rawPath);
|
|
222
|
+
const possibleExts = ['', '.tsx', '.ts', '.jsx', '.js'];
|
|
223
|
+
let resolvedPath = null;
|
|
224
|
+
for (const ext of possibleExts) {
|
|
225
|
+
const candidate = filePath + ext;
|
|
226
|
+
if (await fs_extra_1.default.pathExists(candidate)) {
|
|
227
|
+
resolvedPath = candidate;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (!resolvedPath) {
|
|
232
|
+
res.writeHead(404);
|
|
233
|
+
return res.end(`// File not found: ${filePath}`);
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
let code = await fs_extra_1.default.readFile(resolvedPath, 'utf8');
|
|
237
|
+
// Rewrite bare imports ā /@modules/*
|
|
238
|
+
code = code
|
|
239
|
+
.replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
|
|
240
|
+
.replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
|
|
241
|
+
for (const p of plugins)
|
|
242
|
+
if (p.onTransform)
|
|
243
|
+
code = await p.onTransform(code, resolvedPath);
|
|
244
|
+
const ext = path_1.default.extname(resolvedPath);
|
|
245
|
+
let loader = 'js';
|
|
246
|
+
if (ext === '.ts')
|
|
247
|
+
loader = 'ts';
|
|
248
|
+
else if (ext === '.tsx')
|
|
249
|
+
loader = 'tsx';
|
|
250
|
+
else if (ext === '.jsx')
|
|
251
|
+
loader = 'jsx';
|
|
252
|
+
const result = await esbuild_1.default.transform(code, {
|
|
253
|
+
loader,
|
|
254
|
+
sourcemap: 'inline',
|
|
255
|
+
target: 'es2020',
|
|
256
|
+
});
|
|
257
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
258
|
+
res.end(result.code);
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
const e = err;
|
|
262
|
+
console.error(chalk_1.default.red(`ā ļø Transform failed: ${e.message}`));
|
|
263
|
+
res.writeHead(500);
|
|
264
|
+
res.end(`// Error: ${e.message}`);
|
|
265
|
+
}
|
|
274
266
|
});
|
|
275
|
-
//
|
|
267
|
+
// š¼ļø Serve static assets (favicon + public)
|
|
276
268
|
app.use(async (req, res, next) => {
|
|
277
269
|
if (!req.url)
|
|
278
270
|
return next();
|
|
279
|
-
const assetPath = decodeURIComponent(req.url.split('?')[0]);
|
|
280
271
|
const publicDir = path_1.default.join(appRoot, 'public');
|
|
281
|
-
const
|
|
282
|
-
|
|
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)
|
|
272
|
+
const targetFile = path_1.default.join(publicDir, decodeURIComponent(req.url.split('?')[0]));
|
|
273
|
+
if (!(await fs_extra_1.default.pathExists(targetFile)))
|
|
289
274
|
return next();
|
|
290
275
|
const stat = await fs_extra_1.default.stat(targetFile);
|
|
291
276
|
if (!stat.isFile())
|
|
292
277
|
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();
|
|
297
|
-
}
|
|
298
|
-
res.setHeader('Cache-Control', 'public, max-age=3600');
|
|
299
|
-
res.setHeader('ETag', etag);
|
|
300
278
|
res.setHeader('Content-Type', getMimeType(targetFile));
|
|
301
279
|
fs_extra_1.default.createReadStream(targetFile).pipe(res);
|
|
302
280
|
});
|
|
303
|
-
//
|
|
281
|
+
// š§© Serve index.html + overlay + HMR
|
|
304
282
|
app.use(async (req, res, next) => {
|
|
305
283
|
if (req.url !== '/' && req.url !== '/index.html')
|
|
306
284
|
return next();
|
|
@@ -310,29 +288,7 @@ async function dev() {
|
|
|
310
288
|
}
|
|
311
289
|
let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
|
|
312
290
|
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>
|
|
291
|
+
<script type="module" src="/@runtime/overlay-runtime.js"></script>
|
|
336
292
|
<script type="module">
|
|
337
293
|
const ws = new WebSocket("ws://" + location.host);
|
|
338
294
|
ws.onmessage = (e) => {
|
|
@@ -349,7 +305,7 @@ async function dev() {
|
|
|
349
305
|
res.setHeader('Content-Type', 'text/html');
|
|
350
306
|
res.end(html);
|
|
351
307
|
});
|
|
352
|
-
//
|
|
308
|
+
// ā»ļø Watchers
|
|
353
309
|
chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', (file) => {
|
|
354
310
|
console.log(chalk_1.default.yellow(`š Changed: ${file}`));
|
|
355
311
|
transformCache.delete(file);
|
|
@@ -359,11 +315,11 @@ async function dev() {
|
|
|
359
315
|
});
|
|
360
316
|
});
|
|
361
317
|
chokidar_1.default
|
|
362
|
-
.watch(path_1.default.join(appRoot, '
|
|
318
|
+
.watch(path_1.default.join(appRoot, 'src/runtime/overlay-runtime.js'), { ignoreInitial: true })
|
|
363
319
|
.on('change', () => {
|
|
320
|
+
console.log(chalk_1.default.magenta('ā»ļø Overlay runtime updated ā reloading browser...'));
|
|
364
321
|
broadcaster.broadcast({ type: 'reload' });
|
|
365
322
|
});
|
|
366
|
-
// --- Start server
|
|
367
323
|
server.listen(port, async () => {
|
|
368
324
|
const url = `http://localhost:${port}`;
|
|
369
325
|
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
|
});
|