react-client 1.0.20 ā 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/README.md +3 -3
- package/dist/cli/commands/dev.js +174 -186
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/preview.js +134 -27
- 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
|
+
* ------------------------------------------
|
|
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
|
+
*/
|
|
2
12
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
13
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
14
|
};
|
|
@@ -15,111 +25,67 @@ 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
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
const computeHash = (content) => crypto_1.default.createHash('sha1').update(content).digest('hex');
|
|
33
|
+
const getMimeType = (file) => {
|
|
34
|
+
const ext = path_1.default.extname(file).toLowerCase();
|
|
35
|
+
const mime = {
|
|
36
|
+
'.ico': 'image/x-icon',
|
|
37
|
+
'.png': 'image/png',
|
|
38
|
+
'.jpg': 'image/jpeg',
|
|
39
|
+
'.jpeg': 'image/jpeg',
|
|
40
|
+
'.gif': 'image/gif',
|
|
41
|
+
'.svg': 'image/svg+xml',
|
|
42
|
+
'.webp': 'image/webp',
|
|
43
|
+
'.json': 'application/json',
|
|
44
|
+
'.txt': 'text/plain',
|
|
45
|
+
'.js': 'application/javascript',
|
|
46
|
+
'.mjs': 'application/javascript',
|
|
47
|
+
'.css': 'text/css',
|
|
48
|
+
'.html': 'text/html',
|
|
49
|
+
};
|
|
50
|
+
return mime[ext] || 'application/octet-stream';
|
|
36
51
|
};
|
|
37
|
-
// List of NPM packages required for polyfills
|
|
38
|
-
const POLYFILL_PACKAGES = [
|
|
39
|
-
'buffer',
|
|
40
|
-
'process',
|
|
41
|
-
'path-browserify',
|
|
42
|
-
'browserify-fs',
|
|
43
|
-
'os-browserify',
|
|
44
|
-
'stream-browserify',
|
|
45
|
-
'util',
|
|
46
|
-
'url',
|
|
47
|
-
'assert',
|
|
48
|
-
'crypto-browserify',
|
|
49
|
-
'events',
|
|
50
|
-
'constants-browserify',
|
|
51
|
-
'querystring-es3',
|
|
52
|
-
'browserify-zlib',
|
|
53
|
-
];
|
|
54
52
|
async function dev() {
|
|
55
53
|
const root = process.cwd();
|
|
56
54
|
const userConfig = await (0, loadConfig_1.loadReactClientConfig)(root);
|
|
57
55
|
const appRoot = path_1.default.resolve(root, userConfig.root || '.');
|
|
58
56
|
const defaultPort = userConfig.server?.port || 5173;
|
|
59
57
|
const cacheDir = path_1.default.join(appRoot, '.react-client', 'deps');
|
|
58
|
+
const pkgFile = path_1.default.join(appRoot, 'package.json');
|
|
59
|
+
const indexHtml = path_1.default.join(appRoot, 'index.html');
|
|
60
60
|
await fs_extra_1.default.ensureDir(cacheDir);
|
|
61
|
-
// Detect entry
|
|
61
|
+
// ā
Detect entry
|
|
62
62
|
const possibleEntries = ['src/main.tsx', 'src/main.jsx'];
|
|
63
63
|
const entry = possibleEntries.map((p) => path_1.default.join(appRoot, p)).find((p) => fs_extra_1.default.existsSync(p));
|
|
64
64
|
if (!entry) {
|
|
65
65
|
console.error(chalk_1.default.red('ā No entry found: src/main.tsx or src/main.jsx'));
|
|
66
66
|
process.exit(1);
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const port = availablePort;
|
|
72
|
-
if (availablePort !== defaultPort) {
|
|
68
|
+
// ā
Detect free port
|
|
69
|
+
const port = await (0, detect_port_1.default)(defaultPort);
|
|
70
|
+
if (port !== defaultPort) {
|
|
73
71
|
const res = await (0, prompts_1.default)({
|
|
74
72
|
type: 'confirm',
|
|
75
73
|
name: 'useNewPort',
|
|
76
|
-
message: `Port ${defaultPort} is occupied. Use ${
|
|
74
|
+
message: `Port ${defaultPort} is occupied. Use ${port} instead?`,
|
|
77
75
|
initial: true,
|
|
78
76
|
});
|
|
79
|
-
if (!res.useNewPort)
|
|
80
|
-
console.log('š Dev server cancelled.');
|
|
77
|
+
if (!res.useNewPort)
|
|
81
78
|
process.exit(0);
|
|
82
|
-
}
|
|
83
79
|
}
|
|
84
|
-
//
|
|
80
|
+
// ā
Ensure react-refresh
|
|
85
81
|
try {
|
|
86
82
|
require.resolve('react-refresh/runtime');
|
|
87
83
|
}
|
|
88
84
|
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.'));
|
|
95
|
-
}
|
|
96
|
-
// š§© Auto-install missing polyfill packages
|
|
97
|
-
const missingPolyfills = POLYFILL_PACKAGES.filter((pkg) => {
|
|
98
|
-
try {
|
|
99
|
-
require.resolve(pkg, { paths: [appRoot] });
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
if (missingPolyfills.length > 0) {
|
|
107
|
-
console.log(chalk_1.default.yellow('āļø Installing missing polyfill packages...'));
|
|
108
|
-
console.log(chalk_1.default.gray('š¦ ' + missingPolyfills.join(', ')));
|
|
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
|
-
}
|
|
85
|
+
console.log(chalk_1.default.yellow('Installing react-refresh...'));
|
|
86
|
+
(0, child_process_1.execSync)('npm i react-refresh --silent', { cwd: root, stdio: 'inherit' });
|
|
121
87
|
}
|
|
122
|
-
//
|
|
88
|
+
// ā
Core + user plugins
|
|
123
89
|
const corePlugins = [
|
|
124
90
|
{
|
|
125
91
|
name: 'css-hmr',
|
|
@@ -141,109 +107,141 @@ async function dev() {
|
|
|
141
107
|
const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
|
|
142
108
|
const plugins = [...corePlugins, ...userPlugins];
|
|
143
109
|
const app = (0, connect_1.default)();
|
|
110
|
+
const server = http_1.default.createServer(app);
|
|
111
|
+
const broadcaster = new broadcastManager_1.BroadcastManager(server);
|
|
144
112
|
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];
|
|
113
|
+
// š§± Persistent prebundle cache
|
|
114
|
+
async function prebundleDeps() {
|
|
115
|
+
if (!(await fs_extra_1.default.pathExists(pkgFile)))
|
|
116
|
+
return;
|
|
117
|
+
const pkg = JSON.parse(await fs_extra_1.default.readFile(pkgFile, 'utf8'));
|
|
118
|
+
const deps = Object.keys(pkg.dependencies || {});
|
|
119
|
+
if (!deps.length)
|
|
120
|
+
return;
|
|
121
|
+
const hash = computeHash(JSON.stringify(deps));
|
|
122
|
+
const metaFile = path_1.default.join(cacheDir, '_meta.json');
|
|
123
|
+
let prevHash = null;
|
|
124
|
+
if (await fs_extra_1.default.pathExists(metaFile))
|
|
125
|
+
prevHash = (await fs_extra_1.default.readJSON(metaFile)).hash;
|
|
126
|
+
if (prevHash === hash)
|
|
127
|
+
return;
|
|
128
|
+
console.log(chalk_1.default.cyan('š¦ Rebuilding prebundle cache...'));
|
|
129
|
+
await Promise.all(deps.map(async (dep) => {
|
|
173
130
|
try {
|
|
174
|
-
entryPath = require.resolve(
|
|
131
|
+
const entryPath = require.resolve(dep, { paths: [appRoot] });
|
|
132
|
+
const outFile = path_1.default.join(cacheDir, dep + '.js');
|
|
133
|
+
await esbuild_1.default.build({
|
|
134
|
+
entryPoints: [entryPath],
|
|
135
|
+
bundle: true,
|
|
136
|
+
platform: 'browser',
|
|
137
|
+
format: 'esm',
|
|
138
|
+
target: 'es2020',
|
|
139
|
+
outfile: outFile,
|
|
140
|
+
write: true,
|
|
141
|
+
});
|
|
142
|
+
const content = await fs_extra_1.default.readFile(outFile);
|
|
143
|
+
await fs_extra_1.default.writeFile(outFile + '.gz', zlib_1.default.gzipSync(content));
|
|
144
|
+
await fs_extra_1.default.writeFile(outFile + '.br', zlib_1.default.brotliCompressSync(content));
|
|
145
|
+
console.log(chalk_1.default.green(`ā
Cached ${dep}`));
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
const err = e;
|
|
149
|
+
console.warn(chalk_1.default.yellow(`ā ļø Skipped ${dep}: ${err.message}`));
|
|
175
150
|
}
|
|
176
|
-
|
|
177
|
-
|
|
151
|
+
}));
|
|
152
|
+
await fs_extra_1.default.writeJSON(metaFile, { hash });
|
|
153
|
+
}
|
|
154
|
+
await prebundleDeps();
|
|
155
|
+
chokidar_1.default.watch(pkgFile).on('change', prebundleDeps);
|
|
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}`);
|
|
178
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);
|
|
179
176
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
`;
|
|
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}`);
|
|
201
182
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
// --- /@modules/
|
|
183
|
+
});
|
|
184
|
+
// š§ Serve /@modules/
|
|
206
185
|
app.use('/@modules/', async (req, res, next) => {
|
|
207
186
|
const id = req.url?.replace(/^\/(@modules\/)?/, '');
|
|
208
187
|
if (!id)
|
|
209
188
|
return next();
|
|
210
189
|
try {
|
|
211
|
-
const
|
|
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));
|
|
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');
|
|
212
206
|
res.setHeader('Content-Type', 'application/javascript');
|
|
213
207
|
res.end(code);
|
|
214
208
|
}
|
|
215
|
-
catch (
|
|
216
|
-
const
|
|
217
|
-
console.error(chalk_1.default.red(`ā Failed to load module ${id}: ${
|
|
209
|
+
catch (e) {
|
|
210
|
+
const err = e;
|
|
211
|
+
console.error(chalk_1.default.red(`ā Failed to load module ${id}: ${err.message}`));
|
|
218
212
|
res.writeHead(500);
|
|
219
|
-
res.end(`// Failed to resolve module ${id}: ${
|
|
213
|
+
res.end(`// Failed to resolve module ${id}: ${err.message}`);
|
|
220
214
|
}
|
|
221
215
|
});
|
|
222
|
-
//
|
|
216
|
+
// š§© Serve /src/ and .css files dynamically
|
|
223
217
|
app.use(async (req, res, next) => {
|
|
224
|
-
|
|
225
|
-
if (urlPath.includes('node_modules'))
|
|
218
|
+
if (!req.url || (!req.url.startsWith('/src/') && !req.url.endsWith('.css')))
|
|
226
219
|
return next();
|
|
227
|
-
|
|
228
|
-
const
|
|
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;
|
|
229
224
|
for (const ext of possibleExts) {
|
|
230
|
-
|
|
231
|
-
|
|
225
|
+
const candidate = filePath + ext;
|
|
226
|
+
if (await fs_extra_1.default.pathExists(candidate)) {
|
|
227
|
+
resolvedPath = candidate;
|
|
232
228
|
break;
|
|
233
229
|
}
|
|
234
230
|
}
|
|
235
|
-
if (!
|
|
236
|
-
|
|
231
|
+
if (!resolvedPath) {
|
|
232
|
+
res.writeHead(404);
|
|
233
|
+
return res.end(`// File not found: ${filePath}`);
|
|
234
|
+
}
|
|
237
235
|
try {
|
|
238
|
-
let code = await fs_extra_1.default.readFile(
|
|
239
|
-
// Rewrite bare imports
|
|
236
|
+
let code = await fs_extra_1.default.readFile(resolvedPath, 'utf8');
|
|
237
|
+
// Rewrite bare imports ā /@modules/*
|
|
240
238
|
code = code
|
|
241
239
|
.replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
|
|
242
240
|
.replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
|
|
243
241
|
for (const p of plugins)
|
|
244
242
|
if (p.onTransform)
|
|
245
|
-
code = await p.onTransform(code,
|
|
246
|
-
const ext = path_1.default.extname(
|
|
243
|
+
code = await p.onTransform(code, resolvedPath);
|
|
244
|
+
const ext = path_1.default.extname(resolvedPath);
|
|
247
245
|
let loader = 'js';
|
|
248
246
|
if (ext === '.ts')
|
|
249
247
|
loader = 'ts';
|
|
@@ -256,7 +254,6 @@ async function dev() {
|
|
|
256
254
|
sourcemap: 'inline',
|
|
257
255
|
target: 'es2020',
|
|
258
256
|
});
|
|
259
|
-
transformCache.set(filePath, result.code);
|
|
260
257
|
res.setHeader('Content-Type', 'application/javascript');
|
|
261
258
|
res.end(result.code);
|
|
262
259
|
}
|
|
@@ -267,38 +264,31 @@ async function dev() {
|
|
|
267
264
|
res.end(`// Error: ${e.message}`);
|
|
268
265
|
}
|
|
269
266
|
});
|
|
270
|
-
//
|
|
267
|
+
// š¼ļø Serve static assets (favicon + public)
|
|
268
|
+
app.use(async (req, res, next) => {
|
|
269
|
+
if (!req.url)
|
|
270
|
+
return next();
|
|
271
|
+
const publicDir = path_1.default.join(appRoot, 'public');
|
|
272
|
+
const targetFile = path_1.default.join(publicDir, decodeURIComponent(req.url.split('?')[0]));
|
|
273
|
+
if (!(await fs_extra_1.default.pathExists(targetFile)))
|
|
274
|
+
return next();
|
|
275
|
+
const stat = await fs_extra_1.default.stat(targetFile);
|
|
276
|
+
if (!stat.isFile())
|
|
277
|
+
return next();
|
|
278
|
+
res.setHeader('Content-Type', getMimeType(targetFile));
|
|
279
|
+
fs_extra_1.default.createReadStream(targetFile).pipe(res);
|
|
280
|
+
});
|
|
281
|
+
// š§© Serve index.html + overlay + HMR
|
|
271
282
|
app.use(async (req, res, next) => {
|
|
272
283
|
if (req.url !== '/' && req.url !== '/index.html')
|
|
273
284
|
return next();
|
|
274
|
-
if (!fs_extra_1.default.
|
|
285
|
+
if (!(await fs_extra_1.default.pathExists(indexHtml))) {
|
|
275
286
|
res.writeHead(404);
|
|
276
287
|
return res.end('index.html not found');
|
|
277
288
|
}
|
|
278
289
|
let html = await fs_extra_1.default.readFile(indexHtml, 'utf8');
|
|
279
290
|
html = html.replace('</body>', `
|
|
280
|
-
<script>
|
|
281
|
-
(() => {
|
|
282
|
-
const style = document.createElement('style');
|
|
283
|
-
style.textContent = \`
|
|
284
|
-
.rc-overlay {
|
|
285
|
-
position: fixed; inset: 0; background: rgba(0,0,0,0.9);
|
|
286
|
-
color: #ff5555; font-family: monospace;
|
|
287
|
-
padding: 2rem; overflow:auto; z-index: 999999;
|
|
288
|
-
}
|
|
289
|
-
\`;
|
|
290
|
-
document.head.appendChild(style);
|
|
291
|
-
window.showErrorOverlay = (err) => {
|
|
292
|
-
window.clearErrorOverlay?.();
|
|
293
|
-
const el = document.createElement('div');
|
|
294
|
-
el.className = 'rc-overlay';
|
|
295
|
-
el.innerHTML = '<h2>šØ Error</h2><pre>' + (err.message || err) + '</pre>';
|
|
296
|
-
document.body.appendChild(el);
|
|
297
|
-
window.__overlay = el;
|
|
298
|
-
};
|
|
299
|
-
window.clearErrorOverlay = () => window.__overlay?.remove();
|
|
300
|
-
})();
|
|
301
|
-
</script>
|
|
291
|
+
<script type="module" src="/@runtime/overlay-runtime.js"></script>
|
|
302
292
|
<script type="module">
|
|
303
293
|
const ws = new WebSocket("ws://" + location.host);
|
|
304
294
|
ws.onmessage = (e) => {
|
|
@@ -315,28 +305,26 @@ async function dev() {
|
|
|
315
305
|
res.setHeader('Content-Type', 'text/html');
|
|
316
306
|
res.end(html);
|
|
317
307
|
});
|
|
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;
|
|
308
|
+
// ā»ļø Watchers
|
|
309
|
+
chokidar_1.default.watch(path_1.default.join(appRoot, 'src'), { ignoreInitial: true }).on('change', (file) => {
|
|
324
310
|
console.log(chalk_1.default.yellow(`š Changed: ${file}`));
|
|
325
311
|
transformCache.delete(file);
|
|
326
|
-
for (const p of plugins)
|
|
327
|
-
await p.onHotUpdate?.(file, { broadcast: (msg) => broadcaster.broadcast(msg) });
|
|
328
312
|
broadcaster.broadcast({
|
|
329
313
|
type: 'update',
|
|
330
314
|
path: '/' + path_1.default.relative(appRoot, file).replace(/\\/g, '/'),
|
|
331
315
|
});
|
|
332
316
|
});
|
|
317
|
+
chokidar_1.default
|
|
318
|
+
.watch(path_1.default.join(appRoot, 'src/runtime/overlay-runtime.js'), { ignoreInitial: true })
|
|
319
|
+
.on('change', () => {
|
|
320
|
+
console.log(chalk_1.default.magenta('ā»ļø Overlay runtime updated ā reloading browser...'));
|
|
321
|
+
broadcaster.broadcast({ type: 'reload' });
|
|
322
|
+
});
|
|
333
323
|
server.listen(port, async () => {
|
|
334
324
|
const url = `http://localhost:${port}`;
|
|
335
325
|
console.log(chalk_1.default.cyan.bold('\nš React Client Dev Server'));
|
|
336
326
|
console.log(chalk_1.default.gray('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
337
327
|
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
328
|
await (0, open_1.default)(url, { newInstance: true });
|
|
341
329
|
});
|
|
342
330
|
process.on('SIGINT', () => {
|
|
@@ -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
|
});
|