react-client 1.0.37 → 1.0.40
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 +115 -124
- package/dist/cli/commands/build.js +19 -3
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/dev.d.ts +5 -6
- package/dist/cli/commands/dev.js +505 -231
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.js +67 -7
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/preview.js +9 -15
- package/dist/cli/commands/preview.js.map +1 -1
- package/dist/cli/index.js +1 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/server/broadcastManager.d.ts +18 -20
- package/dist/server/broadcastManager.js +13 -33
- package/dist/server/broadcastManager.js.map +1 -1
- package/dist/utils/loadConfig.js +31 -22
- package/dist/utils/loadConfig.js.map +1 -1
- package/package.json +3 -2
- package/templates/react/public/favicon.ico +0 -0
- package/templates/react/public/index.html +14 -0
- package/templates/react/public/logo512.png +0 -0
- package/templates/react/src/App.css +42 -0
- package/templates/react/src/App.jsx +23 -1
- package/templates/react/src/index.css +68 -0
- package/templates/react-tailwind/public/favicon.ico +0 -0
- package/templates/react-tailwind/public/index.html +14 -0
- package/templates/react-tailwind/public/logo512.png +0 -0
- package/templates/react-tailwind/src/App.css +42 -0
- package/templates/react-tailwind/src/App.jsx +31 -2
- package/templates/react-tailwind/src/index.css +68 -1
- package/templates/react-tailwind/src/main.jsx +1 -3
- package/templates/react-tailwind-ts/public/favicon.ico +0 -0
- package/templates/react-tailwind-ts/public/index.html +14 -0
- package/templates/react-tailwind-ts/public/logo512.png +0 -0
- package/templates/react-tailwind-ts/src/App.css +42 -0
- package/templates/react-tailwind-ts/src/App.tsx +30 -2
- package/templates/react-tailwind-ts/src/index.css +68 -1
- package/templates/react-tailwind-ts/src/main.tsx +0 -1
- package/templates/react-ts/public/favicon.ico +0 -0
- package/templates/react-ts/public/index.html +14 -0
- package/templates/react-ts/public/logo512.png +0 -0
- package/templates/react-ts/src/App.css +42 -0
- package/templates/react-ts/src/App.tsx +23 -1
- package/templates/react-ts/src/index.css +68 -0
- package/templates/react/index.html +0 -13
- package/templates/react-tailwind/index.html +0 -13
- package/templates/react-tailwind-ts/index.html +0 -13
- package/templates/react-ts/index.html +0 -13
package/dist/cli/commands/dev.js
CHANGED
|
@@ -1,48 +1,51 @@
|
|
|
1
|
-
// src/cli/commands/dev.ts
|
|
2
1
|
/**
|
|
3
|
-
* dev.ts —
|
|
2
|
+
* dev.ts — dev server for react-client
|
|
4
3
|
*
|
|
5
|
-
* - resolves package export fields & subpaths (resolveModuleEntry)
|
|
6
4
|
* - prebundles deps into .react-client/deps
|
|
7
|
-
* - serves /@modules/<dep>
|
|
8
|
-
* - serves /src/* with esbuild transform
|
|
9
|
-
* - serves local overlay runtime at /@runtime/overlay if src/runtime/overlay-runtime.js exists
|
|
5
|
+
* - serves /@modules/<dep>
|
|
6
|
+
* - serves /src/* with esbuild transform & inline sourcemap
|
|
10
7
|
* - /@source-map returns a snippet for overlay mapping
|
|
11
8
|
* - HMR broadcast via BroadcastManager (ws)
|
|
12
|
-
*
|
|
9
|
+
*
|
|
10
|
+
* Keep this file linted & typed. Avoids manual react-dom/client hacks.
|
|
13
11
|
*/
|
|
14
12
|
import esbuild from 'esbuild';
|
|
15
13
|
import connect from 'connect';
|
|
16
14
|
import http from 'http';
|
|
17
15
|
import chokidar from 'chokidar';
|
|
18
16
|
import detectPort from 'detect-port';
|
|
19
|
-
import prompts from 'prompts';
|
|
20
17
|
import path from 'path';
|
|
21
18
|
import fs from 'fs-extra';
|
|
22
19
|
import open from 'open';
|
|
23
20
|
import chalk from 'chalk';
|
|
24
21
|
import { execSync } from 'child_process';
|
|
25
|
-
import { loadReactClientConfig } from '../../utils/loadConfig.js';
|
|
26
22
|
import { BroadcastManager } from '../../server/broadcastManager.js';
|
|
27
|
-
|
|
23
|
+
import { createRequire } from 'module';
|
|
24
|
+
import { fileURLToPath } from 'url';
|
|
25
|
+
import { dirname } from 'path';
|
|
26
|
+
import { loadReactClientConfig } from '../../utils/loadConfig.js';
|
|
27
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
28
|
+
const __dirname = dirname(__filename);
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
28
30
|
function jsContentType() {
|
|
29
31
|
return 'application/javascript; charset=utf-8';
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
|
-
* Resolve
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* Resolve any bare import id robustly:
|
|
35
|
+
* 1. try require.resolve(id)
|
|
36
|
+
* 2. try require.resolve(`${pkg}/${subpath}`)
|
|
37
|
+
* 3. try package.json exports field
|
|
38
|
+
* 4. try common fallback candidates
|
|
36
39
|
*/
|
|
37
40
|
async function resolveModuleEntry(id, root) {
|
|
38
|
-
// quick
|
|
41
|
+
// quick resolution
|
|
39
42
|
try {
|
|
40
43
|
return require.resolve(id, { paths: [root] });
|
|
41
44
|
}
|
|
42
45
|
catch {
|
|
43
46
|
// continue
|
|
44
47
|
}
|
|
45
|
-
// split package root and
|
|
48
|
+
// split package root and subpath
|
|
46
49
|
const parts = id.split('/');
|
|
47
50
|
const pkgRoot = parts[0].startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
48
51
|
const subPath = parts.slice(pkgRoot.startsWith('@') ? 2 : 1).join('/');
|
|
@@ -51,100 +54,120 @@ async function resolveModuleEntry(id, root) {
|
|
|
51
54
|
pkgJsonPath = require.resolve(`${pkgRoot}/package.json`, { paths: [root] });
|
|
52
55
|
}
|
|
53
56
|
catch {
|
|
57
|
+
// No need to keep unused variable 'err'
|
|
54
58
|
throw new Error(`Package not found: ${pkgRoot}`);
|
|
55
59
|
}
|
|
56
60
|
const pkgDir = path.dirname(pkgJsonPath);
|
|
61
|
+
// Explicitly type pkgJson to avoid 'any'
|
|
57
62
|
let pkgJson = {};
|
|
58
63
|
try {
|
|
59
|
-
const
|
|
60
|
-
pkgJson = JSON.parse(
|
|
64
|
+
const pkgContent = await fs.readFile(pkgJsonPath, 'utf8');
|
|
65
|
+
pkgJson = JSON.parse(pkgContent);
|
|
61
66
|
}
|
|
62
67
|
catch {
|
|
63
|
-
|
|
68
|
+
// ignore parse or read errors gracefully
|
|
64
69
|
}
|
|
65
|
-
//
|
|
70
|
+
// If exports field exists, try to look up subpath (type-safe, supports conditional exports)
|
|
66
71
|
if (pkgJson.exports) {
|
|
67
72
|
const exportsField = pkgJson.exports;
|
|
73
|
+
// If exports is a plain string -> it's the entry
|
|
68
74
|
if (typeof exportsField === 'string') {
|
|
69
|
-
if (!subPath)
|
|
70
|
-
|
|
71
|
-
if (await fs.pathExists(candidate))
|
|
72
|
-
return candidate;
|
|
73
|
-
}
|
|
75
|
+
if (!subPath)
|
|
76
|
+
return path.resolve(pkgDir, exportsField);
|
|
74
77
|
}
|
|
75
78
|
else if (exportsField && typeof exportsField === 'object') {
|
|
79
|
+
// Normalize to a record so we can index it safely
|
|
76
80
|
const exportsMap = exportsField;
|
|
77
|
-
|
|
81
|
+
// Try candidates in order: explicit subpath, index, fallback
|
|
82
|
+
const keyCandidates = [];
|
|
78
83
|
if (subPath) {
|
|
79
|
-
|
|
84
|
+
keyCandidates.push(`./${subPath}`, `./${subPath}.js`, `./${subPath}.mjs`);
|
|
80
85
|
}
|
|
81
|
-
|
|
82
|
-
for (const key of
|
|
86
|
+
keyCandidates.push('.', './index.js', './index.mjs');
|
|
87
|
+
for (const key of keyCandidates) {
|
|
83
88
|
if (!(key in exportsMap))
|
|
84
89
|
continue;
|
|
85
90
|
const entry = exportsMap[key];
|
|
91
|
+
// entry may be string or object like { import: "...", require: "..." }
|
|
86
92
|
let target;
|
|
87
|
-
if (typeof entry === 'string')
|
|
93
|
+
if (typeof entry === 'string') {
|
|
88
94
|
target = entry;
|
|
95
|
+
}
|
|
89
96
|
else if (entry && typeof entry === 'object') {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
const entryObj = entry;
|
|
98
|
+
// Prefer "import" field for ESM consumers, then "default", then any string-ish value
|
|
99
|
+
if (typeof entryObj.import === 'string')
|
|
100
|
+
target = entryObj.import;
|
|
101
|
+
else if (typeof entryObj.default === 'string')
|
|
102
|
+
target = entryObj.default;
|
|
95
103
|
else {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
// If the entry object itself is a conditional map (like {"node": "...", "browser": "..."}),
|
|
105
|
+
// attempt to pick any string value present.
|
|
106
|
+
for (const k of Object.keys(entryObj)) {
|
|
107
|
+
if (typeof entryObj[k] === 'string') {
|
|
108
|
+
target = entryObj[k];
|
|
99
109
|
break;
|
|
100
110
|
}
|
|
101
111
|
}
|
|
102
112
|
}
|
|
103
113
|
}
|
|
104
|
-
if (!target)
|
|
114
|
+
if (!target || typeof target !== 'string')
|
|
105
115
|
continue;
|
|
116
|
+
// Normalize relative paths in exports (remove leading ./)
|
|
106
117
|
const normalized = target.replace(/^\.\//, '');
|
|
107
|
-
const abs = path.isAbsolute(normalized)
|
|
108
|
-
|
|
118
|
+
const abs = path.isAbsolute(normalized)
|
|
119
|
+
? normalized
|
|
120
|
+
: path.resolve(pkgDir, normalized);
|
|
121
|
+
if (await fs.pathExists(abs)) {
|
|
109
122
|
return abs;
|
|
123
|
+
}
|
|
110
124
|
}
|
|
111
125
|
}
|
|
112
126
|
}
|
|
113
|
-
//
|
|
127
|
+
// Try resolved subpath directly (pkg/subpath)
|
|
114
128
|
if (subPath) {
|
|
115
129
|
try {
|
|
116
|
-
|
|
130
|
+
const candidate = require.resolve(`${pkgRoot}/${subPath}`, { paths: [root] });
|
|
131
|
+
return candidate;
|
|
117
132
|
}
|
|
118
133
|
catch {
|
|
119
|
-
//
|
|
120
|
-
const
|
|
134
|
+
// fallback to searching common candidates under package dir
|
|
135
|
+
const candPaths = [
|
|
121
136
|
path.join(pkgDir, subPath),
|
|
122
|
-
path.join(pkgDir,
|
|
123
|
-
path.join(pkgDir,
|
|
137
|
+
path.join(pkgDir, subPath + '.js'),
|
|
138
|
+
path.join(pkgDir, subPath + '.mjs'),
|
|
124
139
|
path.join(pkgDir, subPath, 'index.js'),
|
|
125
140
|
path.join(pkgDir, subPath, 'index.mjs'),
|
|
126
141
|
];
|
|
127
|
-
for (const c of
|
|
142
|
+
for (const c of candPaths) {
|
|
128
143
|
if (await fs.pathExists(c))
|
|
129
144
|
return c;
|
|
130
145
|
}
|
|
131
146
|
}
|
|
132
147
|
}
|
|
133
|
-
//
|
|
134
|
-
const
|
|
148
|
+
// Try package's main/module/browser fields safely (typed as string)
|
|
149
|
+
const candidateFields = [
|
|
135
150
|
typeof pkgJson.module === 'string' ? pkgJson.module : undefined,
|
|
136
151
|
typeof pkgJson.browser === 'string' ? pkgJson.browser : undefined,
|
|
137
152
|
typeof pkgJson.main === 'string' ? pkgJson.main : undefined,
|
|
138
153
|
];
|
|
139
|
-
for (const
|
|
140
|
-
if (!
|
|
154
|
+
for (const field of candidateFields) {
|
|
155
|
+
if (!field)
|
|
141
156
|
continue;
|
|
142
|
-
const abs = path.isAbsolute(
|
|
157
|
+
const abs = path.isAbsolute(field) ? field : path.resolve(pkgDir, field);
|
|
143
158
|
if (await fs.pathExists(abs))
|
|
144
159
|
return abs;
|
|
145
160
|
}
|
|
146
|
-
throw new Error(`Could not resolve module entry for
|
|
161
|
+
throw new Error(`Could not resolve module entry for ${id}`);
|
|
147
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Wrap the built module for subpath imports:
|
|
165
|
+
* For requests like "/@modules/react-dom/client" — we bundle the resolved file
|
|
166
|
+
* and return it. If the user requested the package root instead, the resolved
|
|
167
|
+
* bundle is returned directly.
|
|
168
|
+
*
|
|
169
|
+
* No hardcoded special cases.
|
|
170
|
+
*/
|
|
148
171
|
function normalizeCacheKey(id) {
|
|
149
172
|
return id.replace(/[\\/]/g, '_');
|
|
150
173
|
}
|
|
@@ -152,31 +175,39 @@ export default async function dev() {
|
|
|
152
175
|
const root = process.cwd();
|
|
153
176
|
const userConfig = (await loadReactClientConfig(root));
|
|
154
177
|
const appRoot = path.resolve(root, userConfig.root || '.');
|
|
155
|
-
const defaultPort = userConfig.server?.port
|
|
178
|
+
const defaultPort = Number(process.env.PORT) || userConfig.server?.port || 2202;
|
|
179
|
+
// cache dir for prebundled deps
|
|
156
180
|
const cacheDir = path.join(appRoot, '.react-client', 'deps');
|
|
157
181
|
await fs.ensureDir(cacheDir);
|
|
158
|
-
// Detect entry
|
|
159
|
-
const
|
|
160
|
-
|
|
182
|
+
// Detect entry (main.tsx / main.jsx)
|
|
183
|
+
const paths = [
|
|
184
|
+
path.join(appRoot, 'src/main.tsx'),
|
|
185
|
+
path.join(appRoot, 'src/main.jsx'),
|
|
186
|
+
path.join(appRoot, 'main.tsx'),
|
|
187
|
+
path.join(appRoot, 'main.jsx'),
|
|
188
|
+
];
|
|
189
|
+
const entry = paths.find((p) => fs.existsSync(p));
|
|
161
190
|
if (!entry) {
|
|
162
|
-
console.error(chalk.red('❌ Entry not found:
|
|
191
|
+
console.error(chalk.red('❌ Entry not found: main.tsx or main.jsx in app root or src/'));
|
|
163
192
|
process.exit(1);
|
|
164
193
|
}
|
|
165
|
-
|
|
166
|
-
|
|
194
|
+
// Detect index.html and public dir
|
|
195
|
+
let publicDir = path.join(appRoot, 'public');
|
|
196
|
+
if (!fs.existsSync(publicDir)) {
|
|
197
|
+
publicDir = path.join(root, 'public');
|
|
198
|
+
if (!fs.existsSync(publicDir)) {
|
|
199
|
+
// Create empty if missing, but usually templates provide it
|
|
200
|
+
await fs.ensureDir(publicDir);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const indexHtml = path.join(publicDir, 'index.html');
|
|
204
|
+
// Select port
|
|
167
205
|
const availablePort = await detectPort(defaultPort);
|
|
168
206
|
const port = availablePort;
|
|
169
207
|
if (availablePort !== defaultPort) {
|
|
170
|
-
|
|
171
|
-
type: 'confirm',
|
|
172
|
-
name: 'useNewPort',
|
|
173
|
-
message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
|
|
174
|
-
initial: true,
|
|
175
|
-
});
|
|
176
|
-
if (!res.useNewPort)
|
|
177
|
-
process.exit(0);
|
|
208
|
+
console.log(chalk.yellow(`\n⚠️ Port ${defaultPort} is occupied. Using ${availablePort} instead.`));
|
|
178
209
|
}
|
|
179
|
-
//
|
|
210
|
+
// Ensure react-refresh runtime available (used by many templates)
|
|
180
211
|
try {
|
|
181
212
|
require.resolve('react-refresh/runtime');
|
|
182
213
|
}
|
|
@@ -189,10 +220,10 @@ export default async function dev() {
|
|
|
189
220
|
});
|
|
190
221
|
}
|
|
191
222
|
catch {
|
|
192
|
-
console.warn(chalk.yellow('⚠️
|
|
223
|
+
console.warn(chalk.yellow('⚠️ automatic install of react-refresh failed; continuing without it.'));
|
|
193
224
|
}
|
|
194
225
|
}
|
|
195
|
-
//
|
|
226
|
+
// Plugin system (core + user)
|
|
196
227
|
const corePlugins = [
|
|
197
228
|
{
|
|
198
229
|
name: 'css-hmr',
|
|
@@ -201,93 +232,285 @@ export default async function dev() {
|
|
|
201
232
|
const escaped = JSON.stringify(code);
|
|
202
233
|
return `
|
|
203
234
|
const css = ${escaped};
|
|
204
|
-
const style = document.createElement(
|
|
235
|
+
const style = document.createElement("style");
|
|
205
236
|
style.textContent = css;
|
|
206
237
|
document.head.appendChild(style);
|
|
207
|
-
|
|
238
|
+
import.meta.hot?.accept();
|
|
208
239
|
`;
|
|
209
240
|
}
|
|
210
241
|
return code;
|
|
211
242
|
},
|
|
212
243
|
},
|
|
244
|
+
{
|
|
245
|
+
name: 'react-refresh',
|
|
246
|
+
async onTransform(code, id) {
|
|
247
|
+
if (id.match(/\.[tj]sx$/)) {
|
|
248
|
+
// In ESM, we can't easily put statements before imports.
|
|
249
|
+
// We'll rely on the global hook injected in index.html.
|
|
250
|
+
const relativePath = '/' + path.relative(appRoot, id);
|
|
251
|
+
const hmrBoilerplate = `
|
|
252
|
+
if (window.__REFRESH_RUNTIME__ && window.__GET_HOT_CONTEXT__) {
|
|
253
|
+
const ___hot = window.__GET_HOT_CONTEXT__(${JSON.stringify(relativePath)});
|
|
254
|
+
if (___hot) {
|
|
255
|
+
window.$RefreshReg$ = (type, id) => {
|
|
256
|
+
window.__REFRESH_RUNTIME__.register(type, ${JSON.stringify(relativePath)} + " " + id);
|
|
257
|
+
};
|
|
258
|
+
window.$RefreshSig$ = () => window.__REFRESH_RUNTIME__.createSignatureFunctionForTransform();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
`;
|
|
262
|
+
const modBoilerplate = `
|
|
263
|
+
if (window.__RC_HMR_STATE__) {
|
|
264
|
+
const ___mod = window.__RC_HMR_STATE__.modules[${JSON.stringify(relativePath)}];
|
|
265
|
+
if (___mod && ___mod.cb) {
|
|
266
|
+
if (typeof ___mod.cb === 'function') ___mod.cb();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
`;
|
|
270
|
+
return `${code}\n${hmrBoilerplate}\n${modBoilerplate}`;
|
|
271
|
+
}
|
|
272
|
+
return code;
|
|
273
|
+
},
|
|
274
|
+
},
|
|
213
275
|
];
|
|
214
276
|
const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
|
|
215
277
|
const plugins = [...corePlugins, ...userPlugins];
|
|
216
|
-
//
|
|
278
|
+
// App + caches
|
|
217
279
|
const app = connect();
|
|
218
280
|
const transformCache = new Map();
|
|
219
|
-
// dependency
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
281
|
+
// Helper: recursively analyze dependency graph for prebundling (bare imports)
|
|
282
|
+
// --- Dependency Analysis & Prebundling ---
|
|
283
|
+
async function analyzeGraph(file, _seen = new Set()) {
|
|
284
|
+
const deps = new Set();
|
|
285
|
+
const visitedFiles = new Set();
|
|
286
|
+
async function walk(f) {
|
|
287
|
+
if (visitedFiles.has(f))
|
|
288
|
+
return;
|
|
289
|
+
visitedFiles.add(f);
|
|
290
|
+
try {
|
|
291
|
+
const code = await fs.readFile(f, 'utf8');
|
|
292
|
+
const matches = [
|
|
293
|
+
...code.matchAll(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g),
|
|
294
|
+
...code.matchAll(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g),
|
|
295
|
+
...code.matchAll(/\brequire\(['"]([^'".\/][^'"]*)['"]\)/g),
|
|
296
|
+
];
|
|
297
|
+
for (const m of matches) {
|
|
298
|
+
const dep = m[1];
|
|
299
|
+
if (!dep || dep.startsWith('.') || dep.startsWith('/'))
|
|
300
|
+
continue;
|
|
301
|
+
if (!deps.has(dep)) {
|
|
302
|
+
deps.add(dep);
|
|
303
|
+
try {
|
|
304
|
+
const resolved = require.resolve(dep, { paths: [appRoot] });
|
|
305
|
+
if (resolved.includes('node_modules')) {
|
|
306
|
+
await walk(resolved);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// skip unresolvable
|
|
311
|
+
}
|
|
312
|
+
}
|
|
240
313
|
}
|
|
241
314
|
}
|
|
315
|
+
catch {
|
|
316
|
+
// skip missing files
|
|
317
|
+
}
|
|
242
318
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
return seen;
|
|
319
|
+
await walk(file);
|
|
320
|
+
return deps;
|
|
247
321
|
}
|
|
322
|
+
// Helper: esbuild plugin to rewrite bare imports in dependency bundles to /@modules/
|
|
323
|
+
const dependencyBundlePlugin = {
|
|
324
|
+
name: 'dependency-bundle-plugin',
|
|
325
|
+
setup(build) {
|
|
326
|
+
// Intercept any bare import (not starting with . or /) that is NOT the entry point
|
|
327
|
+
build.onResolve({ filter: /^[^.\/]/ }, (args) => {
|
|
328
|
+
// If this is the initial entry point, don't externalize it
|
|
329
|
+
if (args.kind === 'entry-point')
|
|
330
|
+
return null;
|
|
331
|
+
// Otherwise, externalize and point to /@modules/
|
|
332
|
+
return {
|
|
333
|
+
path: `/@modules/${args.path}`,
|
|
334
|
+
external: true,
|
|
335
|
+
};
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
// Prebundle dependencies into cache dir using code-splitting
|
|
248
340
|
async function prebundleDeps(deps) {
|
|
249
341
|
if (!deps.size)
|
|
250
342
|
return;
|
|
251
|
-
const
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
343
|
+
const entryPoints = {};
|
|
344
|
+
const depsArray = [...deps];
|
|
345
|
+
// Create a temp directory for proxy files
|
|
346
|
+
const proxyDir = path.join(appRoot, '.react-client', 'proxies');
|
|
347
|
+
await fs.ensureDir(proxyDir);
|
|
348
|
+
for (const dep of depsArray) {
|
|
257
349
|
try {
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
350
|
+
const resolved = require.resolve(dep, { paths: [appRoot] });
|
|
351
|
+
const key = normalizeCacheKey(dep);
|
|
352
|
+
const proxyPath = path.join(proxyDir, `${key}.js`);
|
|
353
|
+
const resolvedPath = JSON.stringify(resolved);
|
|
354
|
+
let proxyCode = '';
|
|
355
|
+
// Precision Proxy: hardcoded exports for most critical React dependencies
|
|
356
|
+
const reactKeys = [
|
|
357
|
+
'useState',
|
|
358
|
+
'useEffect',
|
|
359
|
+
'useContext',
|
|
360
|
+
'useReducer',
|
|
361
|
+
'useCallback',
|
|
362
|
+
'useMemo',
|
|
363
|
+
'useRef',
|
|
364
|
+
'useImperativeHandle',
|
|
365
|
+
'useLayoutEffect',
|
|
366
|
+
'useDebugValue',
|
|
367
|
+
'useDeferredValue',
|
|
368
|
+
'useTransition',
|
|
369
|
+
'useId',
|
|
370
|
+
'useInsertionEffect',
|
|
371
|
+
'useSyncExternalStore',
|
|
372
|
+
'createElement',
|
|
373
|
+
'createContext',
|
|
374
|
+
'createRef',
|
|
375
|
+
'forwardRef',
|
|
376
|
+
'memo',
|
|
377
|
+
'lazy',
|
|
378
|
+
'Suspense',
|
|
379
|
+
'Fragment',
|
|
380
|
+
'Profiler',
|
|
381
|
+
'StrictMode',
|
|
382
|
+
'Children',
|
|
383
|
+
'Component',
|
|
384
|
+
'PureComponent',
|
|
385
|
+
'cloneElement',
|
|
386
|
+
'isValidElement',
|
|
387
|
+
'createFactory',
|
|
388
|
+
'version',
|
|
389
|
+
'startTransition',
|
|
390
|
+
];
|
|
391
|
+
const reactDomClientKeys = ['createRoot', 'hydrateRoot'];
|
|
392
|
+
const reactDomKeys = [
|
|
393
|
+
'render',
|
|
394
|
+
'hydrate',
|
|
395
|
+
'unmountComponentAtNode',
|
|
396
|
+
'findDOMNode',
|
|
397
|
+
'createPortal',
|
|
398
|
+
'version',
|
|
399
|
+
'flushSync',
|
|
400
|
+
];
|
|
401
|
+
const jsxRuntimeKeys = ['jsx', 'jsxs', 'Fragment'];
|
|
402
|
+
if (dep === 'react') {
|
|
403
|
+
proxyCode = `import * as m from ${resolvedPath}; export const { ${reactKeys.join(', ')} } = m; export default (m.default || m);`;
|
|
404
|
+
}
|
|
405
|
+
else if (dep === 'react-dom/client') {
|
|
406
|
+
proxyCode = `import * as m from ${resolvedPath}; export const { ${reactDomClientKeys.join(', ')} } = m; export default (m.default || m);`;
|
|
407
|
+
}
|
|
408
|
+
else if (dep === 'react-dom') {
|
|
409
|
+
proxyCode = `import * as m from ${resolvedPath}; export const { ${reactDomKeys.join(', ')} } = m; export default (m.default || m);`;
|
|
410
|
+
}
|
|
411
|
+
else if (dep === 'react/jsx-runtime' || dep === 'react/jsx-dev-runtime') {
|
|
412
|
+
proxyCode = `import * as m from ${resolvedPath}; export const { ${jsxRuntimeKeys.join(', ')} } = m; export default (m.default || m);`;
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
try {
|
|
416
|
+
// Dynamic Proxy Generation for other deps
|
|
417
|
+
const m = require(resolved);
|
|
418
|
+
const keys = Object.keys(m).filter((k) => k !== 'default' && k !== '__esModule');
|
|
419
|
+
if (keys.length > 0) {
|
|
420
|
+
proxyCode = `import * as m from ${resolvedPath}; export const { ${keys.join(', ')} } = m; export default (m.default || m);`;
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
proxyCode = `import _default from ${resolvedPath}; export default _default;`;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
proxyCode = `export * from ${resolvedPath}; import _default from ${resolvedPath}; export default _default;`;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
await fs.writeFile(proxyPath, proxyCode, 'utf8');
|
|
431
|
+
entryPoints[key] = proxyPath;
|
|
270
432
|
}
|
|
271
433
|
catch (err) {
|
|
272
|
-
console.warn(chalk.yellow(`⚠️
|
|
434
|
+
console.warn(chalk.yellow(`⚠️ Could not resolve ${dep}: ${err.message}`));
|
|
273
435
|
}
|
|
274
|
-
}
|
|
436
|
+
}
|
|
437
|
+
if (Object.keys(entryPoints).length === 0)
|
|
438
|
+
return;
|
|
439
|
+
console.log(chalk.cyan('📦 Prebundling dependencies with precision proxies...'));
|
|
440
|
+
try {
|
|
441
|
+
await esbuild.build({
|
|
442
|
+
entryPoints,
|
|
443
|
+
bundle: true,
|
|
444
|
+
splitting: true, // Re-enable splitting for shared dependency chunks
|
|
445
|
+
format: 'esm',
|
|
446
|
+
outdir: cacheDir,
|
|
447
|
+
platform: 'browser',
|
|
448
|
+
target: ['es2020'],
|
|
449
|
+
minify: false,
|
|
450
|
+
plugins: [], // NO external plugins during prebundle, let esbuild manage the graph
|
|
451
|
+
define: {
|
|
452
|
+
'process.env.NODE_ENV': '"development"',
|
|
453
|
+
},
|
|
454
|
+
logLevel: 'error',
|
|
455
|
+
});
|
|
456
|
+
// Cleanup proxy dir after build
|
|
457
|
+
await fs.remove(proxyDir).catch(() => { });
|
|
458
|
+
console.log(chalk.green('✅ Prebundling complete.'));
|
|
459
|
+
}
|
|
460
|
+
catch (err) {
|
|
461
|
+
console.error(chalk.red(`❌ Prebundling failed: ${err.message}`));
|
|
462
|
+
}
|
|
275
463
|
}
|
|
276
|
-
// initial prebundle
|
|
464
|
+
// Build initial prebundle graph from entry
|
|
277
465
|
const depsSet = await analyzeGraph(entry);
|
|
466
|
+
// Ensure react/jsx-runtime is prebundled if used
|
|
467
|
+
depsSet.add('react/jsx-runtime');
|
|
278
468
|
await prebundleDeps(depsSet);
|
|
279
|
-
//
|
|
469
|
+
// Watch package.json for changes to re-prebundle
|
|
280
470
|
const pkgPath = path.join(appRoot, 'package.json');
|
|
281
471
|
if (await fs.pathExists(pkgPath)) {
|
|
282
472
|
chokidar.watch(pkgPath).on('change', async () => {
|
|
283
473
|
console.log(chalk.yellow('📦 package.json changed — rebuilding prebundle...'));
|
|
284
474
|
const newDeps = await analyzeGraph(entry);
|
|
475
|
+
newDeps.add('react/jsx-runtime');
|
|
285
476
|
await prebundleDeps(newDeps);
|
|
286
477
|
});
|
|
287
478
|
}
|
|
288
|
-
// --- Serve /@modules
|
|
479
|
+
// --- Serve /@modules/<dep> (prebundled or on-demand esbuild bundle)
|
|
289
480
|
app.use((async (req, res, next) => {
|
|
290
481
|
const url = req.url ?? '';
|
|
482
|
+
// Serve React Refresh runtime
|
|
483
|
+
if (url === '/@react-refresh') {
|
|
484
|
+
res.setHeader('Content-Type', jsContentType());
|
|
485
|
+
try {
|
|
486
|
+
const runtimePath = require.resolve('react-refresh/runtime');
|
|
487
|
+
// Bundle it to ESM for the browser
|
|
488
|
+
const bundled = await esbuild.build({
|
|
489
|
+
entryPoints: [runtimePath],
|
|
490
|
+
bundle: true,
|
|
491
|
+
format: 'iife',
|
|
492
|
+
globalName: '__REFRESH_RUNTIME__',
|
|
493
|
+
write: false,
|
|
494
|
+
minify: true,
|
|
495
|
+
define: {
|
|
496
|
+
'process.env.NODE_ENV': '"development"',
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
const runtimeCode = bundled.outputFiles?.[0]?.text ?? '';
|
|
500
|
+
return res.end(`
|
|
501
|
+
const prevRefreshReg = window.$RefreshReg$;
|
|
502
|
+
const prevRefreshSig = window.$RefreshSig$;
|
|
503
|
+
${runtimeCode}
|
|
504
|
+
window.$RefreshReg$ = prevRefreshReg;
|
|
505
|
+
window.$RefreshSig$ = prevRefreshSig;
|
|
506
|
+
export default window.__REFRESH_RUNTIME__;
|
|
507
|
+
`);
|
|
508
|
+
}
|
|
509
|
+
catch (err) {
|
|
510
|
+
res.writeHead(500);
|
|
511
|
+
return res.end(`// react-refresh runtime error: ${err.message}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
291
514
|
if (!url.startsWith('/@modules/'))
|
|
292
515
|
return next();
|
|
293
516
|
const id = url.replace(/^\/@modules\//, '');
|
|
@@ -296,12 +519,25 @@ export default async function dev() {
|
|
|
296
519
|
return res.end('// invalid module');
|
|
297
520
|
}
|
|
298
521
|
try {
|
|
299
|
-
|
|
522
|
+
// 1. Check if it's a file in the cache directory (prebundled or shared chunk)
|
|
523
|
+
// Chunks might be requested via /@modules/dep/chunk-xxx.js or just /@modules/chunk-xxx.js
|
|
524
|
+
const idBase = path.basename(id);
|
|
525
|
+
const cacheFile = id.endsWith('.js')
|
|
526
|
+
? path.join(cacheDir, id)
|
|
527
|
+
: path.join(cacheDir, normalizeCacheKey(id) + '.js');
|
|
528
|
+
const cacheFileAlternative = path.join(cacheDir, idBase);
|
|
529
|
+
let foundCacheFile = '';
|
|
300
530
|
if (await fs.pathExists(cacheFile)) {
|
|
531
|
+
foundCacheFile = cacheFile;
|
|
532
|
+
}
|
|
533
|
+
else if (await fs.pathExists(cacheFileAlternative)) {
|
|
534
|
+
foundCacheFile = cacheFileAlternative;
|
|
535
|
+
}
|
|
536
|
+
if (foundCacheFile) {
|
|
301
537
|
res.setHeader('Content-Type', jsContentType());
|
|
302
|
-
return res.end(await fs.readFile(
|
|
538
|
+
return res.end(await fs.readFile(foundCacheFile, 'utf8'));
|
|
303
539
|
}
|
|
304
|
-
// Resolve
|
|
540
|
+
// 2. Resolve the actual entry file for bare imports
|
|
305
541
|
const entryFile = await resolveModuleEntry(id, appRoot);
|
|
306
542
|
const result = await esbuild.build({
|
|
307
543
|
entryPoints: [entryFile],
|
|
@@ -310,8 +546,15 @@ export default async function dev() {
|
|
|
310
546
|
format: 'esm',
|
|
311
547
|
write: false,
|
|
312
548
|
target: ['es2020'],
|
|
549
|
+
jsx: 'automatic',
|
|
550
|
+
// Critical: use dependencyBundlePlugin to ensure sub-deps are rewritten to /@modules/
|
|
551
|
+
plugins: [dependencyBundlePlugin],
|
|
552
|
+
define: {
|
|
553
|
+
'process.env.NODE_ENV': '"development"',
|
|
554
|
+
},
|
|
313
555
|
});
|
|
314
556
|
const output = result.outputFiles?.[0]?.text ?? '';
|
|
557
|
+
// Write cache and respond
|
|
315
558
|
await fs.writeFile(cacheFile, output, 'utf8');
|
|
316
559
|
res.setHeader('Content-Type', jsContentType());
|
|
317
560
|
res.end(output);
|
|
@@ -321,27 +564,15 @@ export default async function dev() {
|
|
|
321
564
|
res.end(`// Failed to resolve module ${id}: ${err.message}`);
|
|
322
565
|
}
|
|
323
566
|
}));
|
|
324
|
-
// --- Serve
|
|
325
|
-
const
|
|
326
|
-
app.use(async (req, res, next) => {
|
|
327
|
-
if (req.url !== RUNTIME_OVERLAY_ROUTE)
|
|
328
|
-
return next();
|
|
329
|
-
try {
|
|
330
|
-
if (await fs.pathExists(localOverlayPath)) {
|
|
331
|
-
res.setHeader('Content-Type', jsContentType());
|
|
332
|
-
return res.end(await fs.readFile(localOverlayPath, 'utf8'));
|
|
333
|
-
}
|
|
334
|
-
// Inline fallback runtime (minimal)
|
|
335
|
-
const inlineRuntime = `
|
|
336
|
-
/* Inline overlay fallback (auto-generated) */
|
|
337
|
-
${(() => {
|
|
338
|
-
return `
|
|
567
|
+
// --- Serve runtime overlay (inline, no external dependencies)
|
|
568
|
+
const OVERLAY_RUNTIME = `
|
|
339
569
|
const overlayId = "__rc_error_overlay__";
|
|
340
|
-
(function(){
|
|
341
|
-
const style = document.createElement(
|
|
570
|
+
(function(){
|
|
571
|
+
const style = document.createElement("style");
|
|
342
572
|
style.textContent = \`
|
|
343
573
|
#\${overlayId}{position:fixed;inset:0;background:rgba(0,0,0,0.9);color:#fff;font-family:Menlo,Consolas,monospace;font-size:14px;z-index:999999;overflow:auto;padding:24px;}
|
|
344
574
|
#\${overlayId} h2{color:#ff6b6b;margin-bottom:16px;}
|
|
575
|
+
#\${overlayId} pre{background:rgba(255,255,255,0.06);padding:12px;border-radius:6px;overflow:auto;}
|
|
345
576
|
.frame-file{color:#ffa500;cursor:pointer;font-weight:bold;margin-bottom:4px;}
|
|
346
577
|
.line-number{opacity:0.6;margin-right:10px;display:inline-block;width:2em;text-align:right;}
|
|
347
578
|
\`;
|
|
@@ -349,61 +580,65 @@ const overlayId = "__rc_error_overlay__";
|
|
|
349
580
|
async function mapStackFrame(frame){
|
|
350
581
|
const m = frame.match(/(\\/src\\/[^\s:]+):(\\d+):(\\d+)/);
|
|
351
582
|
if(!m) return frame;
|
|
352
|
-
const [,file,line] = m;
|
|
583
|
+
const [,file,line,col] = m;
|
|
353
584
|
try{
|
|
354
|
-
const resp = await fetch(\`/@source-map?file=\${file}&line=\${line}\`);
|
|
585
|
+
const resp = await fetch(\`/@source-map?file=\${file}&line=\${line}&column=\${col}\`);
|
|
355
586
|
if(!resp.ok) return frame;
|
|
356
587
|
const pos = await resp.json();
|
|
357
588
|
if(pos.source) return pos;
|
|
358
589
|
}catch(e){}
|
|
359
590
|
return frame;
|
|
360
591
|
}
|
|
592
|
+
function highlightSimple(s){
|
|
593
|
+
return s.replace(/(const|let|var|function|return|import|from|export|class|new|await|async|if|else|for|while|try|catch|throw)/g,'<span style="color:#ffb86c">$1</span>');
|
|
594
|
+
}
|
|
361
595
|
async function renderOverlay(err){
|
|
362
596
|
const overlay = document.getElementById(overlayId) || document.body.appendChild(Object.assign(document.createElement("div"),{id:overlayId}));
|
|
363
597
|
overlay.innerHTML = "";
|
|
364
598
|
const title = document.createElement("h2");
|
|
365
599
|
title.textContent = "🔥 " + (err.message || "Error");
|
|
366
600
|
overlay.appendChild(title);
|
|
367
|
-
const frames = (err.stack||"").split("\\n").filter(l
|
|
368
|
-
for(const
|
|
369
|
-
const mapped = await mapStackFrame(
|
|
370
|
-
if(typeof mapped ===
|
|
601
|
+
const frames = (err.stack||"").split("\\n").filter(l => /src\\//.test(l));
|
|
602
|
+
for(const frame of frames){
|
|
603
|
+
const mapped = await mapStackFrame(frame);
|
|
604
|
+
if(typeof mapped === "string") continue;
|
|
605
|
+
const frameEl = document.createElement("div");
|
|
371
606
|
const link = document.createElement("div");
|
|
372
607
|
link.className = "frame-file";
|
|
373
608
|
link.textContent = \`\${mapped.source||mapped.file}:\${mapped.line}:\${mapped.column}\`;
|
|
374
|
-
|
|
609
|
+
link.onclick = ()=>window.open("vscode://file/"+(mapped.source||mapped.file)+":"+mapped.line);
|
|
610
|
+
frameEl.appendChild(link);
|
|
375
611
|
if(mapped.snippet){
|
|
376
612
|
const pre = document.createElement("pre");
|
|
377
|
-
pre.innerHTML = mapped.snippet;
|
|
378
|
-
|
|
613
|
+
pre.innerHTML = highlightSimple(mapped.snippet);
|
|
614
|
+
frameEl.appendChild(pre);
|
|
379
615
|
}
|
|
616
|
+
overlay.appendChild(frameEl);
|
|
380
617
|
}
|
|
381
618
|
}
|
|
382
619
|
window.showErrorOverlay = (err)=>renderOverlay(err);
|
|
383
620
|
window.clearErrorOverlay = ()=>document.getElementById(overlayId)?.remove();
|
|
384
|
-
window.addEventListener("error", e=>window.showErrorOverlay?.(e.error||e));
|
|
385
|
-
window.addEventListener("unhandledrejection", e=>window.showErrorOverlay?.(e.reason||e));
|
|
621
|
+
window.addEventListener("error", e => window.showErrorOverlay?.(e.error || e));
|
|
622
|
+
window.addEventListener("unhandledrejection", e => window.showErrorOverlay?.(e.reason || e));
|
|
386
623
|
})();
|
|
387
624
|
`;
|
|
388
|
-
|
|
389
|
-
|
|
625
|
+
app.use((async (req, res, next) => {
|
|
626
|
+
if (req.url === '/@runtime/overlay') {
|
|
390
627
|
res.setHeader('Content-Type', jsContentType());
|
|
391
|
-
return res.end(
|
|
392
|
-
}
|
|
393
|
-
catch (err) {
|
|
394
|
-
res.writeHead(500);
|
|
395
|
-
res.end(`// overlay serve error: ${err.message}`);
|
|
628
|
+
return res.end(OVERLAY_RUNTIME);
|
|
396
629
|
}
|
|
397
|
-
|
|
398
|
-
|
|
630
|
+
next();
|
|
631
|
+
}));
|
|
632
|
+
// --- minimal /@source-map: return snippet around requested line of original source file
|
|
399
633
|
app.use((async (req, res, next) => {
|
|
400
634
|
const url = req.url ?? '';
|
|
401
635
|
if (!url.startsWith('/@source-map'))
|
|
402
636
|
return next();
|
|
403
637
|
try {
|
|
404
|
-
const parsed = new URL(url, `http://localhost:${port}`);
|
|
638
|
+
const parsed = new URL(req.url ?? '', `http://localhost:${port}`);
|
|
405
639
|
const file = parsed.searchParams.get('file') ?? '';
|
|
406
|
-
const
|
|
640
|
+
const lineStr = parsed.searchParams.get('line') ?? '0';
|
|
641
|
+
const lineNum = Number(lineStr) || 0;
|
|
407
642
|
if (!file) {
|
|
408
643
|
res.writeHead(400);
|
|
409
644
|
return res.end('{}');
|
|
@@ -427,63 +662,87 @@ const overlayId = "__rc_error_overlay__";
|
|
|
427
662
|
})
|
|
428
663
|
.join('\n');
|
|
429
664
|
res.setHeader('Content-Type', 'application/json');
|
|
430
|
-
res.end(JSON.stringify({ source:
|
|
665
|
+
res.end(JSON.stringify({ source: filePath, line: lineNum, column: 0, snippet }));
|
|
431
666
|
}
|
|
432
667
|
catch (err) {
|
|
433
668
|
res.writeHead(500);
|
|
434
669
|
res.end(JSON.stringify({ error: err.message }));
|
|
435
670
|
}
|
|
436
671
|
}));
|
|
672
|
+
// --- Serve public/ files as static assets
|
|
673
|
+
app.use((async (req, res, next) => {
|
|
674
|
+
const raw = decodeURIComponent((req.url ?? '').split('?')[0]);
|
|
675
|
+
const publicFile = path.join(publicDir, raw.replace(/^\//, ''));
|
|
676
|
+
if ((await fs.pathExists(publicFile)) && !(await fs.stat(publicFile)).isDirectory()) {
|
|
677
|
+
const ext = path.extname(publicFile).toLowerCase();
|
|
678
|
+
// Simple content type map
|
|
679
|
+
const types = {
|
|
680
|
+
'.html': 'text/html',
|
|
681
|
+
'.js': 'application/javascript',
|
|
682
|
+
'.css': 'text/css',
|
|
683
|
+
'.json': 'application/json',
|
|
684
|
+
'.png': 'image/png',
|
|
685
|
+
'.jpg': 'image/jpeg',
|
|
686
|
+
'.svg': 'image/svg+xml',
|
|
687
|
+
'.ico': 'image/x-icon',
|
|
688
|
+
};
|
|
689
|
+
const content = await fs.readFile(publicFile);
|
|
690
|
+
res.setHeader('Content-Type', types[ext] || 'application/octet-stream');
|
|
691
|
+
res.setHeader('Content-Length', content.length);
|
|
692
|
+
return res.end(content);
|
|
693
|
+
}
|
|
694
|
+
next();
|
|
695
|
+
}));
|
|
437
696
|
// --- Serve /src/* files (on-the-fly transform + bare import rewrite)
|
|
438
697
|
app.use((async (req, res, next) => {
|
|
439
698
|
const url = req.url ?? '';
|
|
440
699
|
if (!url.startsWith('/src/') && !url.endsWith('.css'))
|
|
441
700
|
return next();
|
|
442
701
|
const raw = decodeURIComponent((req.url ?? '').split('?')[0]);
|
|
443
|
-
const
|
|
702
|
+
const filePath = path.join(appRoot, raw.replace(/^\//, ''));
|
|
703
|
+
// Try file extensions if not exact file
|
|
444
704
|
const exts = ['', '.tsx', '.ts', '.jsx', '.js', '.css'];
|
|
445
705
|
let found = '';
|
|
446
706
|
for (const ext of exts) {
|
|
447
|
-
if (await fs.pathExists(
|
|
448
|
-
found =
|
|
707
|
+
if (await fs.pathExists(filePath + ext)) {
|
|
708
|
+
found = filePath + ext;
|
|
449
709
|
break;
|
|
450
710
|
}
|
|
451
711
|
}
|
|
452
712
|
if (!found)
|
|
453
713
|
return next();
|
|
454
714
|
try {
|
|
455
|
-
// cached transform
|
|
456
|
-
if (transformCache.has(found)) {
|
|
457
|
-
res.setHeader('Content-Type', jsContentType());
|
|
458
|
-
res.end(transformCache.get(found));
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
715
|
let code = await fs.readFile(found, 'utf8');
|
|
462
|
-
//
|
|
463
|
-
code = code
|
|
464
|
-
.replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
|
|
465
|
-
.replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
|
|
466
|
-
// plugin transforms
|
|
716
|
+
// run plugin transforms
|
|
467
717
|
for (const p of plugins) {
|
|
468
718
|
if (p.onTransform) {
|
|
469
|
-
// allow plugin transform to return string
|
|
470
|
-
// eslint-disable-next-line no-await-in-loop
|
|
471
719
|
const out = await p.onTransform(code, found);
|
|
472
720
|
if (typeof out === 'string')
|
|
473
721
|
code = out;
|
|
474
722
|
}
|
|
475
723
|
}
|
|
476
|
-
// loader
|
|
477
724
|
const ext = path.extname(found).toLowerCase();
|
|
478
725
|
const loader = ext === '.ts' ? 'ts' : ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'js';
|
|
479
726
|
const result = await esbuild.transform(code, {
|
|
480
727
|
loader,
|
|
481
728
|
sourcemap: 'inline',
|
|
482
729
|
target: ['es2020'],
|
|
730
|
+
jsx: 'automatic',
|
|
483
731
|
});
|
|
484
|
-
|
|
732
|
+
let transformedCode = result.code;
|
|
733
|
+
// Inject HMR/Refresh boilerplate (ESM-Safe: use global accessors and append logic)
|
|
734
|
+
const modulePath = '/' + path.relative(appRoot, found).replace(/\\/g, '/');
|
|
735
|
+
// 1. Replace import.meta.hot with a global context accessor (safe anywhere in ESM)
|
|
736
|
+
transformedCode = transformedCode.replace(/import\.meta\.hot/g, `window.__GET_HOT_CONTEXT__?.(${JSON.stringify(modulePath)})`);
|
|
737
|
+
// rewrite bare imports -> /@modules/<dep>
|
|
738
|
+
transformedCode = transformedCode
|
|
739
|
+
.replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
|
|
740
|
+
.replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`)
|
|
741
|
+
.replace(/^(import\s+['"])([^'".\/][^'"]*)(['"])/gm, (_m, start, dep, end) => `${start}/@modules/${dep}${end}`)
|
|
742
|
+
.replace(/^(export\s+\*\s+from\s+['"])([^'".\/][^'"]*)(['"])/gm, (_m, start, dep, end) => `${start}/@modules/${dep}${end}`);
|
|
743
|
+
transformCache.set(found, transformedCode);
|
|
485
744
|
res.setHeader('Content-Type', jsContentType());
|
|
486
|
-
res.end(
|
|
745
|
+
res.end(transformedCode);
|
|
487
746
|
}
|
|
488
747
|
catch (err) {
|
|
489
748
|
const e = err;
|
|
@@ -491,7 +750,7 @@ const overlayId = "__rc_error_overlay__";
|
|
|
491
750
|
res.end(`// transform error: ${e.message}`);
|
|
492
751
|
}
|
|
493
752
|
}));
|
|
494
|
-
// --- Serve index.html
|
|
753
|
+
// --- Serve index.html with overlay + HMR client injection
|
|
495
754
|
app.use((async (req, res, next) => {
|
|
496
755
|
const url = req.url ?? '';
|
|
497
756
|
if (url !== '/' && url !== '/index.html')
|
|
@@ -501,9 +760,25 @@ const overlayId = "__rc_error_overlay__";
|
|
|
501
760
|
return res.end('index.html not found');
|
|
502
761
|
}
|
|
503
762
|
try {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
763
|
+
const html = await fs.readFile(indexHtml, 'utf8');
|
|
764
|
+
// React Refresh Preamble for index.html
|
|
765
|
+
const reactRefreshPreamble = `
|
|
766
|
+
<script type="module">
|
|
767
|
+
import RefreshRuntime from "/@react-refresh";
|
|
768
|
+
RefreshRuntime.injectIntoGlobalHook(window);
|
|
769
|
+
window.$RefreshReg$ = () => {};
|
|
770
|
+
window.$RefreshSig$ = () => (type) => type;
|
|
771
|
+
window.__REFRESH_RUNTIME__ = RefreshRuntime;
|
|
772
|
+
</script>
|
|
773
|
+
<script type="module" src="/@runtime/overlay"></script>
|
|
774
|
+
<script type="module">
|
|
775
|
+
window.__RC_HMR_STATE__ = { modules: {} };
|
|
776
|
+
window.__GET_HOT_CONTEXT__ = (id) => {
|
|
777
|
+
return window.__RC_HMR_STATE__.modules[id] || (window.__RC_HMR_STATE__.modules[id] = {
|
|
778
|
+
id,
|
|
779
|
+
accept: (cb) => { window.__RC_HMR_STATE__.modules[id].cb = cb || true; }
|
|
780
|
+
});
|
|
781
|
+
};
|
|
507
782
|
const ws = new WebSocket("ws://" + location.host);
|
|
508
783
|
ws.onmessage = (e) => {
|
|
509
784
|
const msg = JSON.parse(e.data);
|
|
@@ -511,45 +786,55 @@ const overlayId = "__rc_error_overlay__";
|
|
|
511
786
|
if (msg.type === "error") window.showErrorOverlay?.(msg);
|
|
512
787
|
if (msg.type === "update") {
|
|
513
788
|
window.clearErrorOverlay?.();
|
|
514
|
-
|
|
789
|
+
const mod = window.__RC_HMR_STATE__.modules[msg.path];
|
|
790
|
+
if (mod && mod.cb) {
|
|
791
|
+
import(msg.path + "?t=" + Date.now()).then(() => {
|
|
792
|
+
if (typeof mod.cb === 'function') mod.cb();
|
|
793
|
+
// Trigger Fast Refresh after module update
|
|
794
|
+
if (window.__REFRESH_RUNTIME__) {
|
|
795
|
+
window.__REFRESH_RUNTIME__.performReactRefresh();
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
} else {
|
|
799
|
+
location.reload();
|
|
800
|
+
}
|
|
515
801
|
}
|
|
516
802
|
};
|
|
517
|
-
</script
|
|
518
|
-
|
|
803
|
+
</script>`.trim();
|
|
804
|
+
// Inject preamble at the top of <body>
|
|
805
|
+
const newHtml = html.replace('<body>', `<body>\n${reactRefreshPreamble}`);
|
|
519
806
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
520
|
-
res.end(
|
|
807
|
+
res.end(newHtml);
|
|
521
808
|
}
|
|
522
809
|
catch (err) {
|
|
523
810
|
res.writeHead(500);
|
|
524
811
|
res.end(`// html read error: ${err.message}`);
|
|
525
812
|
}
|
|
526
813
|
}));
|
|
527
|
-
// HMR WebSocket
|
|
814
|
+
// --- HMR WebSocket server
|
|
528
815
|
const server = http.createServer(app);
|
|
529
816
|
const broadcaster = new BroadcastManager(server);
|
|
530
|
-
//
|
|
817
|
+
// Watch files and trigger plugin onHotUpdate + broadcast HMR message
|
|
531
818
|
const watcher = chokidar.watch(path.join(appRoot, 'src'), { ignoreInitial: true });
|
|
532
819
|
watcher.on('change', async (file) => {
|
|
533
820
|
transformCache.delete(file);
|
|
821
|
+
// plugin hook onHotUpdate optionally
|
|
534
822
|
for (const p of plugins) {
|
|
535
823
|
if (p.onHotUpdate) {
|
|
536
824
|
try {
|
|
537
|
-
// plugin receives broadcast helper
|
|
538
|
-
// cast to PluginHotUpdateContext (safe wrapper)
|
|
539
|
-
// eslint-disable-next-line no-await-in-loop
|
|
540
825
|
await p.onHotUpdate(file, {
|
|
541
|
-
broadcast
|
|
542
|
-
|
|
826
|
+
// plugin only needs broadcast in most cases
|
|
827
|
+
broadcast: (msg) => {
|
|
828
|
+
broadcaster.broadcast(msg);
|
|
543
829
|
},
|
|
544
830
|
});
|
|
545
831
|
}
|
|
546
832
|
catch (err) {
|
|
547
|
-
// log plugin error but continue
|
|
548
|
-
// eslint-disable-next-line no-console
|
|
549
833
|
console.warn('plugin onHotUpdate error:', err.message);
|
|
550
834
|
}
|
|
551
835
|
}
|
|
552
836
|
}
|
|
837
|
+
// default: broadcast update for changed file
|
|
553
838
|
broadcaster.broadcast({
|
|
554
839
|
type: 'update',
|
|
555
840
|
path: '/' + path.relative(appRoot, file).replace(/\\/g, '/'),
|
|
@@ -559,43 +844,32 @@ const overlayId = "__rc_error_overlay__";
|
|
|
559
844
|
server.listen(port, async () => {
|
|
560
845
|
const url = `http://localhost:${port}`;
|
|
561
846
|
console.log(chalk.cyan.bold('\n🚀 React Client Dev Server'));
|
|
562
|
-
console.log(chalk.gray('──────────────────────────────'));
|
|
563
847
|
console.log(chalk.green(`⚡ Running at: ${url}`));
|
|
564
|
-
|
|
565
|
-
const shouldOpen = userConfig.server?.open !== false;
|
|
566
|
-
if (shouldOpen) {
|
|
848
|
+
if (userConfig.server?.open !== false) {
|
|
567
849
|
try {
|
|
568
850
|
await open(url);
|
|
569
851
|
}
|
|
570
852
|
catch {
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
const ctx = {
|
|
575
|
-
root: appRoot,
|
|
576
|
-
outDir: cacheDir,
|
|
577
|
-
app,
|
|
578
|
-
wss: broadcaster.wss,
|
|
579
|
-
httpServer: server,
|
|
580
|
-
broadcast: (m) => broadcaster.broadcast(m),
|
|
581
|
-
};
|
|
582
|
-
// plugin serve/start hooks
|
|
583
|
-
for (const p of plugins) {
|
|
584
|
-
if (p.onServe) {
|
|
585
|
-
await p.onServe(ctx);
|
|
586
|
-
}
|
|
587
|
-
if (p.onServerStart) {
|
|
588
|
-
await p.onServerStart(ctx);
|
|
853
|
+
// ignore open errors
|
|
589
854
|
}
|
|
590
855
|
}
|
|
591
856
|
});
|
|
592
857
|
// graceful shutdown
|
|
593
|
-
|
|
594
|
-
console.log(chalk.red('\n🛑 Shutting down...'));
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
858
|
+
const shutdown = async () => {
|
|
859
|
+
console.log(chalk.red('\n🛑 Shutting down dev server...'));
|
|
860
|
+
try {
|
|
861
|
+
await watcher.close();
|
|
862
|
+
broadcaster.close();
|
|
863
|
+
server.close();
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
console.error(chalk.red('⚠️ Error during shutdown:'), err.message);
|
|
867
|
+
}
|
|
868
|
+
finally {
|
|
869
|
+
process.exit(0);
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
process.on('SIGINT', shutdown);
|
|
873
|
+
process.on('SIGTERM', shutdown);
|
|
600
874
|
}
|
|
601
875
|
//# sourceMappingURL=dev.js.map
|