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