react-client 1.0.30 ā 1.0.32
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/build.js +32 -44
- package/dist/cli/commands/dev.js +437 -457
- package/dist/cli/commands/init.js +60 -71
- package/dist/cli/commands/preview.js +110 -122
- package/dist/cli/index.js +33 -34
- package/dist/server/broadcastManager.js +1 -1
- package/dist/utils/loadConfig.js +55 -66
- package/package.json +2 -2
package/dist/cli/commands/dev.js
CHANGED
|
@@ -9,15 +9,6 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Keep this file linted & typed. Avoids manual react-dom/client hacks.
|
|
11
11
|
*/
|
|
12
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
13
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
14
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
15
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
16
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
17
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
18
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
19
|
-
});
|
|
20
|
-
};
|
|
21
12
|
import esbuild from 'esbuild';
|
|
22
13
|
import connect from 'connect';
|
|
23
14
|
import http from 'http';
|
|
@@ -29,9 +20,14 @@ import fs from 'fs-extra';
|
|
|
29
20
|
import open from 'open';
|
|
30
21
|
import chalk from 'chalk';
|
|
31
22
|
import { execSync } from 'child_process';
|
|
32
|
-
import { loadReactClientConfig } from '../../utils/loadConfig';
|
|
33
23
|
import { BroadcastManager } from '../../server/broadcastManager';
|
|
34
24
|
import { createRequire } from 'module';
|
|
25
|
+
import { fileURLToPath } from 'url';
|
|
26
|
+
import { dirname, resolve } from 'path';
|
|
27
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
28
|
+
const __dirname = dirname(__filename);
|
|
29
|
+
const loadConfigPath = resolve(__dirname, '../../utils/loadConfig.js');
|
|
30
|
+
const { loadReactClientConfig } = await import(loadConfigPath);
|
|
35
31
|
const require = createRequire(import.meta.url);
|
|
36
32
|
const RUNTIME_OVERLAY_ROUTE = '/@runtime/overlay';
|
|
37
33
|
function jsContentType() {
|
|
@@ -44,130 +40,128 @@ function jsContentType() {
|
|
|
44
40
|
* 3. try package.json exports field
|
|
45
41
|
* 4. try common fallback candidates
|
|
46
42
|
*/
|
|
47
|
-
function resolveModuleEntry(id, root) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
43
|
+
async function resolveModuleEntry(id, root) {
|
|
44
|
+
// quick resolution
|
|
45
|
+
try {
|
|
46
|
+
return require.resolve(id, { paths: [root] });
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// continue
|
|
50
|
+
}
|
|
51
|
+
// split package root and subpath
|
|
52
|
+
const parts = id.split('/');
|
|
53
|
+
const pkgRoot = parts[0].startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
54
|
+
const subPath = parts.slice(pkgRoot.startsWith('@') ? 2 : 1).join('/');
|
|
55
|
+
let pkgJsonPath;
|
|
56
|
+
try {
|
|
57
|
+
pkgJsonPath = require.resolve(`${pkgRoot}/package.json`, { paths: [root] });
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// No need to keep unused variable 'err'
|
|
61
|
+
throw new Error(`Package not found: ${pkgRoot}`);
|
|
62
|
+
}
|
|
63
|
+
const pkgDir = path.dirname(pkgJsonPath);
|
|
64
|
+
// Explicitly type pkgJson to avoid 'any'
|
|
65
|
+
let pkgJson = {};
|
|
66
|
+
try {
|
|
67
|
+
const pkgContent = await fs.readFile(pkgJsonPath, 'utf8');
|
|
68
|
+
pkgJson = JSON.parse(pkgContent);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// ignore parse or read errors gracefully
|
|
72
|
+
}
|
|
73
|
+
// If exports field exists, try to look up subpath (type-safe, supports conditional exports)
|
|
74
|
+
if (pkgJson.exports) {
|
|
75
|
+
const exportsField = pkgJson.exports;
|
|
76
|
+
// If exports is a plain string -> it's the entry
|
|
77
|
+
if (typeof exportsField === 'string') {
|
|
78
|
+
if (!subPath)
|
|
79
|
+
return path.resolve(pkgDir, exportsField);
|
|
77
80
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
else if (exportsField && typeof exportsField === 'object') {
|
|
82
|
+
// Normalize to a record so we can index it safely
|
|
83
|
+
const exportsMap = exportsField;
|
|
84
|
+
// Try candidates in order: explicit subpath, index, fallback
|
|
85
|
+
const keyCandidates = [];
|
|
86
|
+
if (subPath) {
|
|
87
|
+
keyCandidates.push(`./${subPath}`, `./${subPath}.js`, `./${subPath}.mjs`);
|
|
85
88
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
keyCandidates.push('.', './index.js', './index.mjs');
|
|
90
|
+
for (const key of keyCandidates) {
|
|
91
|
+
if (!(key in exportsMap))
|
|
92
|
+
continue;
|
|
93
|
+
const entry = exportsMap[key];
|
|
94
|
+
// entry may be string or object like { import: "...", require: "..." }
|
|
95
|
+
let target;
|
|
96
|
+
if (typeof entry === 'string') {
|
|
97
|
+
target = entry;
|
|
93
98
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
target = entryObj.import;
|
|
109
|
-
else if (typeof entryObj.default === 'string')
|
|
110
|
-
target = entryObj.default;
|
|
111
|
-
else {
|
|
112
|
-
// If the entry object itself is a conditional map (like {"node": "...", "browser": "..."}),
|
|
113
|
-
// attempt to pick any string value present.
|
|
114
|
-
for (const k of Object.keys(entryObj)) {
|
|
115
|
-
if (typeof entryObj[k] === 'string') {
|
|
116
|
-
target = entryObj[k];
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
99
|
+
else if (entry && typeof entry === 'object') {
|
|
100
|
+
const entryObj = entry;
|
|
101
|
+
// Prefer "import" field for ESM consumers, then "default", then any string-ish value
|
|
102
|
+
if (typeof entryObj.import === 'string')
|
|
103
|
+
target = entryObj.import;
|
|
104
|
+
else if (typeof entryObj.default === 'string')
|
|
105
|
+
target = entryObj.default;
|
|
106
|
+
else {
|
|
107
|
+
// If the entry object itself is a conditional map (like {"node": "...", "browser": "..."}),
|
|
108
|
+
// attempt to pick any string value present.
|
|
109
|
+
for (const k of Object.keys(entryObj)) {
|
|
110
|
+
if (typeof entryObj[k] === 'string') {
|
|
111
|
+
target = entryObj[k];
|
|
112
|
+
break;
|
|
119
113
|
}
|
|
120
114
|
}
|
|
121
115
|
}
|
|
122
|
-
if (!target || typeof target !== 'string')
|
|
123
|
-
continue;
|
|
124
|
-
// Normalize relative paths in exports (remove leading ./)
|
|
125
|
-
const normalized = target.replace(/^\.\//, '');
|
|
126
|
-
const abs = path.isAbsolute(normalized)
|
|
127
|
-
? normalized
|
|
128
|
-
: path.resolve(pkgDir, normalized);
|
|
129
|
-
if (yield fs.pathExists(abs)) {
|
|
130
|
-
return abs;
|
|
131
|
-
}
|
|
132
116
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// fallback to searching common candidates under package dir
|
|
143
|
-
const candPaths = [
|
|
144
|
-
path.join(pkgDir, subPath),
|
|
145
|
-
path.join(pkgDir, subPath + '.js'),
|
|
146
|
-
path.join(pkgDir, subPath + '.mjs'),
|
|
147
|
-
path.join(pkgDir, subPath, 'index.js'),
|
|
148
|
-
path.join(pkgDir, subPath, 'index.mjs'),
|
|
149
|
-
];
|
|
150
|
-
for (const c of candPaths) {
|
|
151
|
-
if (yield fs.pathExists(c))
|
|
152
|
-
return c;
|
|
117
|
+
if (!target || typeof target !== 'string')
|
|
118
|
+
continue;
|
|
119
|
+
// Normalize relative paths in exports (remove leading ./)
|
|
120
|
+
const normalized = target.replace(/^\.\//, '');
|
|
121
|
+
const abs = path.isAbsolute(normalized)
|
|
122
|
+
? normalized
|
|
123
|
+
: path.resolve(pkgDir, normalized);
|
|
124
|
+
if (await fs.pathExists(abs)) {
|
|
125
|
+
return abs;
|
|
153
126
|
}
|
|
154
127
|
}
|
|
155
128
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
for (const field of candidateFields) {
|
|
163
|
-
if (!field)
|
|
164
|
-
continue;
|
|
165
|
-
const abs = path.isAbsolute(field) ? field : path.resolve(pkgDir, field);
|
|
166
|
-
if (yield fs.pathExists(abs))
|
|
167
|
-
return abs;
|
|
129
|
+
}
|
|
130
|
+
// Try resolved subpath directly (pkg/subpath)
|
|
131
|
+
if (subPath) {
|
|
132
|
+
try {
|
|
133
|
+
const candidate = require.resolve(`${pkgRoot}/${subPath}`, { paths: [root] });
|
|
134
|
+
return candidate;
|
|
168
135
|
}
|
|
169
|
-
|
|
170
|
-
|
|
136
|
+
catch {
|
|
137
|
+
// fallback to searching common candidates under package dir
|
|
138
|
+
const candPaths = [
|
|
139
|
+
path.join(pkgDir, subPath),
|
|
140
|
+
path.join(pkgDir, subPath + '.js'),
|
|
141
|
+
path.join(pkgDir, subPath + '.mjs'),
|
|
142
|
+
path.join(pkgDir, subPath, 'index.js'),
|
|
143
|
+
path.join(pkgDir, subPath, 'index.mjs'),
|
|
144
|
+
];
|
|
145
|
+
for (const c of candPaths) {
|
|
146
|
+
if (await fs.pathExists(c))
|
|
147
|
+
return c;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Try package's main/module/browser fields safely (typed as string)
|
|
152
|
+
const candidateFields = [
|
|
153
|
+
typeof pkgJson.module === 'string' ? pkgJson.module : undefined,
|
|
154
|
+
typeof pkgJson.browser === 'string' ? pkgJson.browser : undefined,
|
|
155
|
+
typeof pkgJson.main === 'string' ? pkgJson.main : undefined,
|
|
156
|
+
];
|
|
157
|
+
for (const field of candidateFields) {
|
|
158
|
+
if (!field)
|
|
159
|
+
continue;
|
|
160
|
+
const abs = path.isAbsolute(field) ? field : path.resolve(pkgDir, field);
|
|
161
|
+
if (await fs.pathExists(abs))
|
|
162
|
+
return abs;
|
|
163
|
+
}
|
|
164
|
+
throw new Error(`Could not resolve module entry for ${id}`);
|
|
171
165
|
}
|
|
172
166
|
/**
|
|
173
167
|
* Wrap the built module for subpath imports:
|
|
@@ -180,201 +174,192 @@ function resolveModuleEntry(id, root) {
|
|
|
180
174
|
function normalizeCacheKey(id) {
|
|
181
175
|
return id.replace(/[\\/]/g, '_');
|
|
182
176
|
}
|
|
183
|
-
export default function dev() {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
console.log('š Dev server cancelled.');
|
|
213
|
-
process.exit(0);
|
|
214
|
-
}
|
|
177
|
+
export default async function dev() {
|
|
178
|
+
const root = process.cwd();
|
|
179
|
+
const userConfig = (await loadReactClientConfig(root));
|
|
180
|
+
const appRoot = path.resolve(root, userConfig.root || '.');
|
|
181
|
+
const defaultPort = userConfig.server?.port ?? 2202;
|
|
182
|
+
// cache dir for prebundled deps
|
|
183
|
+
const cacheDir = path.join(appRoot, '.react-client', 'deps');
|
|
184
|
+
await fs.ensureDir(cacheDir);
|
|
185
|
+
// Detect entry (main.tsx / main.jsx)
|
|
186
|
+
const possible = ['src/main.tsx', 'src/main.jsx'].map((p) => path.join(appRoot, p));
|
|
187
|
+
const entry = possible.find((p) => fs.existsSync(p));
|
|
188
|
+
if (!entry) {
|
|
189
|
+
console.error(chalk.red('ā Entry not found: src/main.tsx or src/main.jsx'));
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
const indexHtml = path.join(appRoot, 'index.html');
|
|
193
|
+
// Select port
|
|
194
|
+
const availablePort = await detectPort(defaultPort);
|
|
195
|
+
const port = availablePort;
|
|
196
|
+
if (availablePort !== defaultPort) {
|
|
197
|
+
const response = await prompts({
|
|
198
|
+
type: 'confirm',
|
|
199
|
+
name: 'useNewPort',
|
|
200
|
+
message: `Port ${defaultPort} is occupied. Use ${availablePort} instead?`,
|
|
201
|
+
initial: true,
|
|
202
|
+
});
|
|
203
|
+
if (!response.useNewPort) {
|
|
204
|
+
console.log('š Dev server cancelled.');
|
|
205
|
+
process.exit(0);
|
|
215
206
|
}
|
|
216
|
-
|
|
207
|
+
}
|
|
208
|
+
// Ensure react-refresh runtime available (used by many templates)
|
|
209
|
+
try {
|
|
210
|
+
require.resolve('react-refresh/runtime');
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
console.warn(chalk.yellow('ā ļø react-refresh not found ā installing react-refresh...'));
|
|
217
214
|
try {
|
|
218
|
-
|
|
215
|
+
execSync('npm install react-refresh --no-audit --no-fund --silent', {
|
|
216
|
+
cwd: appRoot,
|
|
217
|
+
stdio: 'inherit',
|
|
218
|
+
});
|
|
219
219
|
}
|
|
220
|
-
catch
|
|
221
|
-
console.warn(chalk.yellow('ā ļø react-refresh
|
|
222
|
-
try {
|
|
223
|
-
execSync('npm install react-refresh --no-audit --no-fund --silent', {
|
|
224
|
-
cwd: appRoot,
|
|
225
|
-
stdio: 'inherit',
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
catch (_d) {
|
|
229
|
-
console.warn(chalk.yellow('ā ļø automatic install of react-refresh failed; continuing without it.'));
|
|
230
|
-
}
|
|
220
|
+
catch {
|
|
221
|
+
console.warn(chalk.yellow('ā ļø automatic install of react-refresh failed; continuing without it.'));
|
|
231
222
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
223
|
+
}
|
|
224
|
+
// Plugin system (core + user)
|
|
225
|
+
const corePlugins = [
|
|
226
|
+
{
|
|
227
|
+
name: 'css-hmr',
|
|
228
|
+
async onTransform(code, id) {
|
|
229
|
+
if (id.endsWith('.css')) {
|
|
230
|
+
const escaped = JSON.stringify(code);
|
|
231
|
+
return `
|
|
241
232
|
const css = ${escaped};
|
|
242
233
|
const style = document.createElement("style");
|
|
243
234
|
style.textContent = css;
|
|
244
235
|
document.head.appendChild(style);
|
|
245
236
|
import.meta.hot?.accept();
|
|
246
237
|
`;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
});
|
|
250
|
-
},
|
|
238
|
+
}
|
|
239
|
+
return code;
|
|
251
240
|
},
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
|
|
244
|
+
const plugins = [...corePlugins, ...userPlugins];
|
|
245
|
+
// App + caches
|
|
246
|
+
const app = connect();
|
|
247
|
+
const transformCache = new Map();
|
|
248
|
+
// Helper: recursively analyze dependency graph for prebundling (bare imports)
|
|
249
|
+
async function analyzeGraph(file, seen = new Set()) {
|
|
250
|
+
if (seen.has(file))
|
|
251
|
+
return seen;
|
|
252
|
+
seen.add(file);
|
|
253
|
+
try {
|
|
254
|
+
const code = await fs.readFile(file, 'utf8');
|
|
255
|
+
const matches = [
|
|
256
|
+
...code.matchAll(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g),
|
|
257
|
+
...code.matchAll(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g),
|
|
258
|
+
];
|
|
259
|
+
for (const m of matches) {
|
|
260
|
+
const dep = m[1];
|
|
261
|
+
if (!dep || dep.startsWith('.') || dep.startsWith('/'))
|
|
262
|
+
continue;
|
|
264
263
|
try {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
...code.matchAll(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g),
|
|
268
|
-
...code.matchAll(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g),
|
|
269
|
-
];
|
|
270
|
-
for (const m of matches) {
|
|
271
|
-
const dep = m[1];
|
|
272
|
-
if (!dep || dep.startsWith('.') || dep.startsWith('/'))
|
|
273
|
-
continue;
|
|
274
|
-
try {
|
|
275
|
-
const resolved = require.resolve(dep, { paths: [appRoot] });
|
|
276
|
-
yield analyzeGraph(resolved, seen);
|
|
277
|
-
}
|
|
278
|
-
catch (_a) {
|
|
279
|
-
// bare dependency (node_modules) - track name
|
|
280
|
-
seen.add(dep);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
264
|
+
const resolved = require.resolve(dep, { paths: [appRoot] });
|
|
265
|
+
await analyzeGraph(resolved, seen);
|
|
283
266
|
}
|
|
284
|
-
catch
|
|
285
|
-
//
|
|
267
|
+
catch {
|
|
268
|
+
// bare dependency (node_modules) - track name
|
|
269
|
+
seen.add(dep);
|
|
286
270
|
}
|
|
287
|
-
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
// Prebundle dependencies into cache dir (parallel)
|
|
291
|
-
function prebundleDeps(deps) {
|
|
292
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
293
|
-
if (!deps.size)
|
|
294
|
-
return;
|
|
295
|
-
const existingFiles = yield fs.readdir(cacheDir);
|
|
296
|
-
const existing = new Set(existingFiles.map((f) => f.replace(/\.js$/, '')));
|
|
297
|
-
const missing = [...deps].filter((d) => !existing.has(d));
|
|
298
|
-
if (!missing.length)
|
|
299
|
-
return;
|
|
300
|
-
console.log(chalk.cyan('š¦ Prebundling:'), missing.join(', '));
|
|
301
|
-
yield Promise.all(missing.map((dep) => __awaiter(this, void 0, void 0, function* () {
|
|
302
|
-
try {
|
|
303
|
-
const entryPoint = require.resolve(dep, { paths: [appRoot] });
|
|
304
|
-
const outFile = path.join(cacheDir, normalizeCacheKey(dep) + '.js');
|
|
305
|
-
yield esbuild.build({
|
|
306
|
-
entryPoints: [entryPoint],
|
|
307
|
-
bundle: true,
|
|
308
|
-
platform: 'browser',
|
|
309
|
-
format: 'esm',
|
|
310
|
-
outfile: outFile,
|
|
311
|
-
write: true,
|
|
312
|
-
target: ['es2020'],
|
|
313
|
-
});
|
|
314
|
-
console.log(chalk.green(`ā
Cached ${dep}`));
|
|
315
|
-
}
|
|
316
|
-
catch (err) {
|
|
317
|
-
console.warn(chalk.yellow(`ā ļø Skipped ${dep}: ${err.message}`));
|
|
318
|
-
}
|
|
319
|
-
})));
|
|
320
|
-
});
|
|
271
|
+
}
|
|
321
272
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
yield prebundleDeps(depsSet);
|
|
325
|
-
// Watch package.json for changes to re-prebundle
|
|
326
|
-
const pkgPath = path.join(appRoot, 'package.json');
|
|
327
|
-
if (yield fs.pathExists(pkgPath)) {
|
|
328
|
-
chokidar.watch(pkgPath).on('change', () => __awaiter(this, void 0, void 0, function* () {
|
|
329
|
-
console.log(chalk.yellow('š¦ package.json changed ā rebuilding prebundle...'));
|
|
330
|
-
const newDeps = yield analyzeGraph(entry);
|
|
331
|
-
yield prebundleDeps(newDeps);
|
|
332
|
-
}));
|
|
273
|
+
catch {
|
|
274
|
+
// ignore unreadable files
|
|
333
275
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
276
|
+
return seen;
|
|
277
|
+
}
|
|
278
|
+
// Prebundle dependencies into cache dir (parallel)
|
|
279
|
+
async function prebundleDeps(deps) {
|
|
280
|
+
if (!deps.size)
|
|
281
|
+
return;
|
|
282
|
+
const existingFiles = await fs.readdir(cacheDir);
|
|
283
|
+
const existing = new Set(existingFiles.map((f) => f.replace(/\.js$/, '')));
|
|
284
|
+
const missing = [...deps].filter((d) => !existing.has(d));
|
|
285
|
+
if (!missing.length)
|
|
286
|
+
return;
|
|
287
|
+
console.log(chalk.cyan('š¦ Prebundling:'), missing.join(', '));
|
|
288
|
+
await Promise.all(missing.map(async (dep) => {
|
|
345
289
|
try {
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
// Resolve the actual entry file (handles subpaths & package exports)
|
|
352
|
-
const entryFile = yield resolveModuleEntry(id, appRoot);
|
|
353
|
-
const result = yield esbuild.build({
|
|
354
|
-
entryPoints: [entryFile],
|
|
290
|
+
const entryPoint = require.resolve(dep, { paths: [appRoot] });
|
|
291
|
+
const outFile = path.join(cacheDir, normalizeCacheKey(dep) + '.js');
|
|
292
|
+
await esbuild.build({
|
|
293
|
+
entryPoints: [entryPoint],
|
|
355
294
|
bundle: true,
|
|
356
295
|
platform: 'browser',
|
|
357
296
|
format: 'esm',
|
|
358
|
-
|
|
297
|
+
outfile: outFile,
|
|
298
|
+
write: true,
|
|
359
299
|
target: ['es2020'],
|
|
360
300
|
});
|
|
361
|
-
|
|
362
|
-
// Write cache and respond
|
|
363
|
-
yield fs.writeFile(cacheFile, output, 'utf8');
|
|
364
|
-
res.setHeader('Content-Type', jsContentType());
|
|
365
|
-
res.end(output);
|
|
301
|
+
console.log(chalk.green(`ā
Cached ${dep}`));
|
|
366
302
|
}
|
|
367
303
|
catch (err) {
|
|
368
|
-
|
|
369
|
-
res.end(`// Failed to resolve module ${id}: ${err.message}`);
|
|
304
|
+
console.warn(chalk.yellow(`ā ļø Skipped ${dep}: ${err.message}`));
|
|
370
305
|
}
|
|
371
|
-
}))
|
|
372
|
-
|
|
373
|
-
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
308
|
+
// Build initial prebundle graph from entry
|
|
309
|
+
const depsSet = await analyzeGraph(entry);
|
|
310
|
+
await prebundleDeps(depsSet);
|
|
311
|
+
// Watch package.json for changes to re-prebundle
|
|
312
|
+
const pkgPath = path.join(appRoot, 'package.json');
|
|
313
|
+
if (await fs.pathExists(pkgPath)) {
|
|
314
|
+
chokidar.watch(pkgPath).on('change', async () => {
|
|
315
|
+
console.log(chalk.yellow('š¦ package.json changed ā rebuilding prebundle...'));
|
|
316
|
+
const newDeps = await analyzeGraph(entry);
|
|
317
|
+
await prebundleDeps(newDeps);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
// --- Serve /@modules/<dep> (prebundled or on-demand esbuild bundle)
|
|
321
|
+
app.use((async (req, res, next) => {
|
|
322
|
+
const url = req.url ?? '';
|
|
323
|
+
if (!url.startsWith('/@modules/'))
|
|
324
|
+
return next();
|
|
325
|
+
const id = url.replace(/^\/@modules\//, '');
|
|
326
|
+
if (!id) {
|
|
327
|
+
res.writeHead(400);
|
|
328
|
+
return res.end('// invalid module');
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
const cacheFile = path.join(cacheDir, normalizeCacheKey(id) + '.js');
|
|
332
|
+
if (await fs.pathExists(cacheFile)) {
|
|
333
|
+
res.setHeader('Content-Type', jsContentType());
|
|
334
|
+
return res.end(await fs.readFile(cacheFile, 'utf8'));
|
|
335
|
+
}
|
|
336
|
+
// Resolve the actual entry file (handles subpaths & package exports)
|
|
337
|
+
const entryFile = await resolveModuleEntry(id, appRoot);
|
|
338
|
+
const result = await esbuild.build({
|
|
339
|
+
entryPoints: [entryFile],
|
|
340
|
+
bundle: true,
|
|
341
|
+
platform: 'browser',
|
|
342
|
+
format: 'esm',
|
|
343
|
+
write: false,
|
|
344
|
+
target: ['es2020'],
|
|
345
|
+
});
|
|
346
|
+
const output = result.outputFiles?.[0]?.text ?? '';
|
|
347
|
+
// Write cache and respond
|
|
348
|
+
await fs.writeFile(cacheFile, output, 'utf8');
|
|
349
|
+
res.setHeader('Content-Type', jsContentType());
|
|
350
|
+
res.end(output);
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
res.writeHead(500);
|
|
354
|
+
res.end(`// Failed to resolve module ${id}: ${err.message}`);
|
|
355
|
+
}
|
|
356
|
+
}));
|
|
357
|
+
// --- Serve runtime overlay (inline, no external dependencies)
|
|
358
|
+
const OVERLAY_RUNTIME = `
|
|
374
359
|
/* inline overlay runtime - served at ${RUNTIME_OVERLAY_ROUTE} */
|
|
375
360
|
${(() => {
|
|
376
|
-
|
|
377
|
-
|
|
361
|
+
// small helper ā embed as a string
|
|
362
|
+
return `
|
|
378
363
|
const overlayId = "__rc_error_overlay__";
|
|
379
364
|
(function(){
|
|
380
365
|
const style = document.createElement("style");
|
|
@@ -435,122 +420,119 @@ const overlayId = "__rc_error_overlay__";
|
|
|
435
420
|
window.addEventListener("unhandledrejection", e => window.showErrorOverlay?.(e.reason || e));
|
|
436
421
|
})();
|
|
437
422
|
`;
|
|
438
|
-
|
|
423
|
+
})()}
|
|
439
424
|
`;
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
return res.end('{}');
|
|
461
|
-
}
|
|
462
|
-
const filePath = path.join(appRoot, file.startsWith('/') ? file.slice(1) : file);
|
|
463
|
-
if (!(yield fs.pathExists(filePath))) {
|
|
464
|
-
res.writeHead(404);
|
|
465
|
-
return res.end('{}');
|
|
466
|
-
}
|
|
467
|
-
const src = yield fs.readFile(filePath, 'utf8');
|
|
468
|
-
const lines = src.split(/\r?\n/);
|
|
469
|
-
const start = Math.max(0, lineNum - 3 - 1);
|
|
470
|
-
const end = Math.min(lines.length, lineNum + 2);
|
|
471
|
-
const snippet = lines
|
|
472
|
-
.slice(start, end)
|
|
473
|
-
.map((l, i) => {
|
|
474
|
-
const ln = start + i + 1;
|
|
475
|
-
return `<span class="line-number">${ln}</span> ${l
|
|
476
|
-
.replace(/</g, '<')
|
|
477
|
-
.replace(/>/g, '>')}`;
|
|
478
|
-
})
|
|
479
|
-
.join('\n');
|
|
480
|
-
res.setHeader('Content-Type', 'application/json');
|
|
481
|
-
res.end(JSON.stringify({ source: file, line: lineNum, column: 0, snippet }));
|
|
425
|
+
app.use(async (req, res, next) => {
|
|
426
|
+
if (req.url === RUNTIME_OVERLAY_ROUTE) {
|
|
427
|
+
res.setHeader('Content-Type', jsContentType());
|
|
428
|
+
return res.end(OVERLAY_RUNTIME);
|
|
429
|
+
}
|
|
430
|
+
next();
|
|
431
|
+
});
|
|
432
|
+
// --- minimal /@source-map: return snippet around requested line of original source file
|
|
433
|
+
app.use((async (req, res, next) => {
|
|
434
|
+
const url = req.url ?? '';
|
|
435
|
+
if (!url.startsWith('/@source-map'))
|
|
436
|
+
return next();
|
|
437
|
+
try {
|
|
438
|
+
const parsed = new URL(req.url ?? '', `http://localhost:${port}`);
|
|
439
|
+
const file = parsed.searchParams.get('file') ?? '';
|
|
440
|
+
const lineStr = parsed.searchParams.get('line') ?? '0';
|
|
441
|
+
const lineNum = Number(lineStr) || 0;
|
|
442
|
+
if (!file) {
|
|
443
|
+
res.writeHead(400);
|
|
444
|
+
return res.end('{}');
|
|
482
445
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
res.
|
|
446
|
+
const filePath = path.join(appRoot, file.startsWith('/') ? file.slice(1) : file);
|
|
447
|
+
if (!(await fs.pathExists(filePath))) {
|
|
448
|
+
res.writeHead(404);
|
|
449
|
+
return res.end('{}');
|
|
486
450
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
451
|
+
const src = await fs.readFile(filePath, 'utf8');
|
|
452
|
+
const lines = src.split(/\r?\n/);
|
|
453
|
+
const start = Math.max(0, lineNum - 3 - 1);
|
|
454
|
+
const end = Math.min(lines.length, lineNum + 2);
|
|
455
|
+
const snippet = lines
|
|
456
|
+
.slice(start, end)
|
|
457
|
+
.map((l, i) => {
|
|
458
|
+
const ln = start + i + 1;
|
|
459
|
+
return `<span class="line-number">${ln}</span> ${l
|
|
460
|
+
.replace(/</g, '<')
|
|
461
|
+
.replace(/>/g, '>')}`;
|
|
462
|
+
})
|
|
463
|
+
.join('\n');
|
|
464
|
+
res.setHeader('Content-Type', 'application/json');
|
|
465
|
+
res.end(JSON.stringify({ source: file, line: lineNum, column: 0, snippet }));
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
res.writeHead(500);
|
|
469
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
470
|
+
}
|
|
471
|
+
}));
|
|
472
|
+
// --- Serve /src/* files (on-the-fly transform + bare import rewrite)
|
|
473
|
+
app.use((async (req, res, next) => {
|
|
474
|
+
const url = req.url ?? '';
|
|
475
|
+
if (!url.startsWith('/src/') && !url.endsWith('.css'))
|
|
476
|
+
return next();
|
|
477
|
+
const raw = decodeURIComponent((req.url ?? '').split('?')[0]);
|
|
478
|
+
const filePath = path.join(appRoot, raw.replace(/^\//, ''));
|
|
479
|
+
// Try file extensions if not exact file
|
|
480
|
+
const exts = ['', '.tsx', '.ts', '.jsx', '.js', '.css'];
|
|
481
|
+
let found = '';
|
|
482
|
+
for (const ext of exts) {
|
|
483
|
+
if (await fs.pathExists(filePath + ext)) {
|
|
484
|
+
found = filePath + ext;
|
|
485
|
+
break;
|
|
504
486
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
487
|
+
}
|
|
488
|
+
if (!found)
|
|
489
|
+
return next();
|
|
490
|
+
try {
|
|
491
|
+
let code = await fs.readFile(found, 'utf8');
|
|
492
|
+
// rewrite bare imports -> /@modules/<dep>
|
|
493
|
+
code = code
|
|
494
|
+
.replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
|
|
495
|
+
.replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
|
|
496
|
+
// run plugin transforms
|
|
497
|
+
for (const p of plugins) {
|
|
498
|
+
if (p.onTransform) {
|
|
499
|
+
const out = await p.onTransform(code, found);
|
|
500
|
+
if (typeof out === 'string')
|
|
501
|
+
code = out;
|
|
520
502
|
}
|
|
521
|
-
// choose loader by extension
|
|
522
|
-
const ext = path.extname(found).toLowerCase();
|
|
523
|
-
const loader = ext === '.ts' ? 'ts' : ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'js';
|
|
524
|
-
const result = yield esbuild.transform(code, {
|
|
525
|
-
loader,
|
|
526
|
-
sourcemap: 'inline',
|
|
527
|
-
target: ['es2020'],
|
|
528
|
-
});
|
|
529
|
-
transformCache.set(found, result.code);
|
|
530
|
-
res.setHeader('Content-Type', jsContentType());
|
|
531
|
-
res.end(result.code);
|
|
532
|
-
}
|
|
533
|
-
catch (err) {
|
|
534
|
-
const e = err;
|
|
535
|
-
res.writeHead(500);
|
|
536
|
-
res.end(`// transform error: ${e.message}`);
|
|
537
503
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
504
|
+
// choose loader by extension
|
|
505
|
+
const ext = path.extname(found).toLowerCase();
|
|
506
|
+
const loader = ext === '.ts' ? 'ts' : ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'js';
|
|
507
|
+
const result = await esbuild.transform(code, {
|
|
508
|
+
loader,
|
|
509
|
+
sourcemap: 'inline',
|
|
510
|
+
target: ['es2020'],
|
|
511
|
+
});
|
|
512
|
+
transformCache.set(found, result.code);
|
|
513
|
+
res.setHeader('Content-Type', jsContentType());
|
|
514
|
+
res.end(result.code);
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
const e = err;
|
|
518
|
+
res.writeHead(500);
|
|
519
|
+
res.end(`// transform error: ${e.message}`);
|
|
520
|
+
}
|
|
521
|
+
}));
|
|
522
|
+
// --- Serve index.html with overlay + HMR client injection
|
|
523
|
+
app.use((async (req, res, next) => {
|
|
524
|
+
const url = req.url ?? '';
|
|
525
|
+
if (url !== '/' && url !== '/index.html')
|
|
526
|
+
return next();
|
|
527
|
+
if (!(await fs.pathExists(indexHtml))) {
|
|
528
|
+
res.writeHead(404);
|
|
529
|
+
return res.end('index.html not found');
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
let html = await fs.readFile(indexHtml, 'utf8');
|
|
533
|
+
// inject overlay runtime and HMR client if not already present
|
|
534
|
+
if (!html.includes(RUNTIME_OVERLAY_ROUTE)) {
|
|
535
|
+
html = html.replace('</body>', `\n<script type="module" src="${RUNTIME_OVERLAY_ROUTE}"></script>\n<script type="module">
|
|
554
536
|
const ws = new WebSocket("ws://" + location.host);
|
|
555
537
|
ws.onmessage = (e) => {
|
|
556
538
|
const msg = JSON.parse(e.data);
|
|
@@ -562,66 +544,64 @@ const overlayId = "__rc_error_overlay__";
|
|
|
562
544
|
}
|
|
563
545
|
};
|
|
564
546
|
</script>\n</body>`);
|
|
565
|
-
}
|
|
566
|
-
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
567
|
-
res.end(html);
|
|
568
|
-
}
|
|
569
|
-
catch (err) {
|
|
570
|
-
res.writeHead(500);
|
|
571
|
-
res.end(`// html read error: ${err.message}`);
|
|
572
|
-
}
|
|
573
|
-
})));
|
|
574
|
-
// --- HMR WebSocket server
|
|
575
|
-
const server = http.createServer(app);
|
|
576
|
-
const broadcaster = new BroadcastManager(server);
|
|
577
|
-
// Watch files and trigger plugin onHotUpdate + broadcast HMR message
|
|
578
|
-
const watcher = chokidar.watch(path.join(appRoot, 'src'), { ignoreInitial: true });
|
|
579
|
-
watcher.on('change', (file) => __awaiter(this, void 0, void 0, function* () {
|
|
580
|
-
transformCache.delete(file);
|
|
581
|
-
// plugin hook onHotUpdate optionally
|
|
582
|
-
for (const p of plugins) {
|
|
583
|
-
if (p.onHotUpdate) {
|
|
584
|
-
try {
|
|
585
|
-
yield p.onHotUpdate(file, {
|
|
586
|
-
// plugin only needs broadcast in most cases
|
|
587
|
-
broadcast: (msg) => {
|
|
588
|
-
broadcaster.broadcast(msg);
|
|
589
|
-
},
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
catch (err) {
|
|
593
|
-
console.warn('plugin onHotUpdate error:', err.message);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
547
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
548
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
549
|
+
res.end(html);
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
res.writeHead(500);
|
|
553
|
+
res.end(`// html read error: ${err.message}`);
|
|
554
|
+
}
|
|
555
|
+
}));
|
|
556
|
+
// --- HMR WebSocket server
|
|
557
|
+
const server = http.createServer(app);
|
|
558
|
+
const broadcaster = new BroadcastManager(server);
|
|
559
|
+
// Watch files and trigger plugin onHotUpdate + broadcast HMR message
|
|
560
|
+
const watcher = chokidar.watch(path.join(appRoot, 'src'), { ignoreInitial: true });
|
|
561
|
+
watcher.on('change', async (file) => {
|
|
562
|
+
transformCache.delete(file);
|
|
563
|
+
// plugin hook onHotUpdate optionally
|
|
564
|
+
for (const p of plugins) {
|
|
565
|
+
if (p.onHotUpdate) {
|
|
610
566
|
try {
|
|
611
|
-
|
|
567
|
+
await p.onHotUpdate(file, {
|
|
568
|
+
// plugin only needs broadcast in most cases
|
|
569
|
+
broadcast: (msg) => {
|
|
570
|
+
broadcaster.broadcast(msg);
|
|
571
|
+
},
|
|
572
|
+
});
|
|
612
573
|
}
|
|
613
|
-
catch (
|
|
614
|
-
|
|
574
|
+
catch (err) {
|
|
575
|
+
console.warn('plugin onHotUpdate error:', err.message);
|
|
615
576
|
}
|
|
616
577
|
}
|
|
617
|
-
}
|
|
618
|
-
//
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
578
|
+
}
|
|
579
|
+
// default: broadcast update for changed file
|
|
580
|
+
broadcaster.broadcast({
|
|
581
|
+
type: 'update',
|
|
582
|
+
path: '/' + path.relative(appRoot, file).replace(/\\/g, '/'),
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
// start server
|
|
586
|
+
server.listen(port, async () => {
|
|
587
|
+
const url = `http://localhost:${port}`;
|
|
588
|
+
console.log(chalk.cyan.bold('\nš React Client Dev Server'));
|
|
589
|
+
console.log(chalk.green(`ā” Running at: ${url}`));
|
|
590
|
+
if (userConfig.server?.open !== false) {
|
|
591
|
+
try {
|
|
592
|
+
await open(url);
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
// ignore open errors
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
// graceful shutdown
|
|
600
|
+
process.on('SIGINT', async () => {
|
|
601
|
+
console.log(chalk.red('\nš Shutting down...'));
|
|
602
|
+
await watcher.close();
|
|
603
|
+
broadcaster.close();
|
|
604
|
+
server.close();
|
|
605
|
+
process.exit(0);
|
|
626
606
|
});
|
|
627
607
|
}
|