react-client 1.0.38 → 1.0.41
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.js +373 -102
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.js +72 -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/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
|
@@ -14,7 +14,6 @@ import connect from 'connect';
|
|
|
14
14
|
import http from 'http';
|
|
15
15
|
import chokidar from 'chokidar';
|
|
16
16
|
import detectPort from 'detect-port';
|
|
17
|
-
import prompts from 'prompts';
|
|
18
17
|
import path from 'path';
|
|
19
18
|
import fs from 'fs-extra';
|
|
20
19
|
import open from 'open';
|
|
@@ -23,13 +22,11 @@ import { execSync } from 'child_process';
|
|
|
23
22
|
import { BroadcastManager } from '../../server/broadcastManager.js';
|
|
24
23
|
import { createRequire } from 'module';
|
|
25
24
|
import { fileURLToPath } from 'url';
|
|
26
|
-
import { dirname
|
|
25
|
+
import { dirname } from 'path';
|
|
26
|
+
import { loadReactClientConfig } from '../../utils/loadConfig.js';
|
|
27
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
28
28
|
const __dirname = dirname(__filename);
|
|
29
|
-
const loadConfigPath = resolve(__dirname, '../../utils/loadConfig.js');
|
|
30
|
-
const { loadReactClientConfig } = await import(loadConfigPath);
|
|
31
29
|
const require = createRequire(import.meta.url);
|
|
32
|
-
const RUNTIME_OVERLAY_ROUTE = '/@runtime/overlay';
|
|
33
30
|
function jsContentType() {
|
|
34
31
|
return 'application/javascript; charset=utf-8';
|
|
35
32
|
}
|
|
@@ -178,32 +175,37 @@ export default async function dev() {
|
|
|
178
175
|
const root = process.cwd();
|
|
179
176
|
const userConfig = (await loadReactClientConfig(root));
|
|
180
177
|
const appRoot = path.resolve(root, userConfig.root || '.');
|
|
181
|
-
const defaultPort = userConfig.server?.port
|
|
178
|
+
const defaultPort = Number(process.env.PORT) || userConfig.server?.port || 2202;
|
|
182
179
|
// cache dir for prebundled deps
|
|
183
180
|
const cacheDir = path.join(appRoot, '.react-client', 'deps');
|
|
184
181
|
await fs.ensureDir(cacheDir);
|
|
185
182
|
// Detect entry (main.tsx / main.jsx)
|
|
186
|
-
const
|
|
187
|
-
|
|
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));
|
|
188
190
|
if (!entry) {
|
|
189
|
-
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/'));
|
|
190
192
|
process.exit(1);
|
|
191
193
|
}
|
|
192
|
-
|
|
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');
|
|
193
204
|
// Select port
|
|
194
205
|
const availablePort = await detectPort(defaultPort);
|
|
195
206
|
const port = availablePort;
|
|
196
207
|
if (availablePort !== defaultPort) {
|
|
197
|
-
|
|
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);
|
|
206
|
-
}
|
|
208
|
+
console.log(chalk.yellow(`\n⚠️ Port ${defaultPort} is occupied. Using ${availablePort} instead.`));
|
|
207
209
|
}
|
|
208
210
|
// Ensure react-refresh runtime available (used by many templates)
|
|
209
211
|
try {
|
|
@@ -239,6 +241,37 @@ export default async function dev() {
|
|
|
239
241
|
return code;
|
|
240
242
|
},
|
|
241
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
|
+
},
|
|
242
275
|
];
|
|
243
276
|
const userPlugins = Array.isArray(userConfig.plugins) ? userConfig.plugins : [];
|
|
244
277
|
const plugins = [...corePlugins, ...userPlugins];
|
|
@@ -246,67 +279,192 @@ export default async function dev() {
|
|
|
246
279
|
const app = connect();
|
|
247
280
|
const transformCache = new Map();
|
|
248
281
|
// Helper: recursively analyze dependency graph for prebundling (bare imports)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
}
|
|
270
313
|
}
|
|
271
314
|
}
|
|
315
|
+
catch {
|
|
316
|
+
// skip missing files
|
|
317
|
+
}
|
|
272
318
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
return seen;
|
|
319
|
+
await walk(file);
|
|
320
|
+
return deps;
|
|
277
321
|
}
|
|
278
|
-
//
|
|
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
|
|
279
340
|
async function prebundleDeps(deps) {
|
|
280
341
|
if (!deps.size)
|
|
281
342
|
return;
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
await Promise.all(missing.map(async (dep) => {
|
|
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) {
|
|
289
349
|
try {
|
|
290
|
-
const
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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;
|
|
302
432
|
}
|
|
303
433
|
catch (err) {
|
|
304
|
-
console.warn(chalk.yellow(`⚠️
|
|
434
|
+
console.warn(chalk.yellow(`⚠️ Could not resolve ${dep}: ${err.message}`));
|
|
305
435
|
}
|
|
306
|
-
}
|
|
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
|
+
}
|
|
307
463
|
}
|
|
308
464
|
// Build initial prebundle graph from entry
|
|
309
465
|
const depsSet = await analyzeGraph(entry);
|
|
466
|
+
// Ensure react/jsx-runtime is prebundled if used
|
|
467
|
+
depsSet.add('react/jsx-runtime');
|
|
310
468
|
await prebundleDeps(depsSet);
|
|
311
469
|
// Watch package.json for changes to re-prebundle
|
|
312
470
|
const pkgPath = path.join(appRoot, 'package.json');
|
|
@@ -314,12 +472,45 @@ export default async function dev() {
|
|
|
314
472
|
chokidar.watch(pkgPath).on('change', async () => {
|
|
315
473
|
console.log(chalk.yellow('📦 package.json changed — rebuilding prebundle...'));
|
|
316
474
|
const newDeps = await analyzeGraph(entry);
|
|
475
|
+
newDeps.add('react/jsx-runtime');
|
|
317
476
|
await prebundleDeps(newDeps);
|
|
318
477
|
});
|
|
319
478
|
}
|
|
320
479
|
// --- Serve /@modules/<dep> (prebundled or on-demand esbuild bundle)
|
|
321
480
|
app.use((async (req, res, next) => {
|
|
322
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
|
+
}
|
|
323
514
|
if (!url.startsWith('/@modules/'))
|
|
324
515
|
return next();
|
|
325
516
|
const id = url.replace(/^\/@modules\//, '');
|
|
@@ -328,12 +519,25 @@ export default async function dev() {
|
|
|
328
519
|
return res.end('// invalid module');
|
|
329
520
|
}
|
|
330
521
|
try {
|
|
331
|
-
|
|
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 = '';
|
|
332
530
|
if (await fs.pathExists(cacheFile)) {
|
|
531
|
+
foundCacheFile = cacheFile;
|
|
532
|
+
}
|
|
533
|
+
else if (await fs.pathExists(cacheFileAlternative)) {
|
|
534
|
+
foundCacheFile = cacheFileAlternative;
|
|
535
|
+
}
|
|
536
|
+
if (foundCacheFile) {
|
|
333
537
|
res.setHeader('Content-Type', jsContentType());
|
|
334
|
-
return res.end(await fs.readFile(
|
|
538
|
+
return res.end(await fs.readFile(foundCacheFile, 'utf8'));
|
|
335
539
|
}
|
|
336
|
-
// Resolve the actual entry file
|
|
540
|
+
// 2. Resolve the actual entry file for bare imports
|
|
337
541
|
const entryFile = await resolveModuleEntry(id, appRoot);
|
|
338
542
|
const result = await esbuild.build({
|
|
339
543
|
entryPoints: [entryFile],
|
|
@@ -342,6 +546,12 @@ export default async function dev() {
|
|
|
342
546
|
format: 'esm',
|
|
343
547
|
write: false,
|
|
344
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
|
+
},
|
|
345
555
|
});
|
|
346
556
|
const output = result.outputFiles?.[0]?.text ?? '';
|
|
347
557
|
// Write cache and respond
|
|
@@ -356,10 +566,6 @@ export default async function dev() {
|
|
|
356
566
|
}));
|
|
357
567
|
// --- Serve runtime overlay (inline, no external dependencies)
|
|
358
568
|
const OVERLAY_RUNTIME = `
|
|
359
|
-
/* inline overlay runtime - served at ${RUNTIME_OVERLAY_ROUTE} */
|
|
360
|
-
${(() => {
|
|
361
|
-
// small helper — embed as a string
|
|
362
|
-
return `
|
|
363
569
|
const overlayId = "__rc_error_overlay__";
|
|
364
570
|
(function(){
|
|
365
571
|
const style = document.createElement("style");
|
|
@@ -416,15 +622,13 @@ const overlayId = "__rc_error_overlay__";
|
|
|
416
622
|
window.addEventListener("unhandledrejection", e => window.showErrorOverlay?.(e.reason || e));
|
|
417
623
|
})();
|
|
418
624
|
`;
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
app.use(async (req, res, next) => {
|
|
422
|
-
if (req.url === RUNTIME_OVERLAY_ROUTE) {
|
|
625
|
+
app.use((async (req, res, next) => {
|
|
626
|
+
if (req.url === '/@runtime/overlay') {
|
|
423
627
|
res.setHeader('Content-Type', jsContentType());
|
|
424
628
|
return res.end(OVERLAY_RUNTIME);
|
|
425
629
|
}
|
|
426
630
|
next();
|
|
427
|
-
});
|
|
631
|
+
}));
|
|
428
632
|
// --- minimal /@source-map: return snippet around requested line of original source file
|
|
429
633
|
app.use((async (req, res, next) => {
|
|
430
634
|
const url = req.url ?? '';
|
|
@@ -458,13 +662,37 @@ const overlayId = "__rc_error_overlay__";
|
|
|
458
662
|
})
|
|
459
663
|
.join('\n');
|
|
460
664
|
res.setHeader('Content-Type', 'application/json');
|
|
461
|
-
res.end(JSON.stringify({ source:
|
|
665
|
+
res.end(JSON.stringify({ source: filePath, line: lineNum, column: 0, snippet }));
|
|
462
666
|
}
|
|
463
667
|
catch (err) {
|
|
464
668
|
res.writeHead(500);
|
|
465
669
|
res.end(JSON.stringify({ error: err.message }));
|
|
466
670
|
}
|
|
467
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
|
+
}));
|
|
468
696
|
// --- Serve /src/* files (on-the-fly transform + bare import rewrite)
|
|
469
697
|
app.use((async (req, res, next) => {
|
|
470
698
|
const url = req.url ?? '';
|
|
@@ -485,10 +713,6 @@ const overlayId = "__rc_error_overlay__";
|
|
|
485
713
|
return next();
|
|
486
714
|
try {
|
|
487
715
|
let code = await fs.readFile(found, 'utf8');
|
|
488
|
-
// rewrite bare imports -> /@modules/<dep>
|
|
489
|
-
code = code
|
|
490
|
-
.replace(/\bfrom\s+['"]([^'".\/][^'"]*)['"]/g, (_m, dep) => `from "/@modules/${dep}"`)
|
|
491
|
-
.replace(/\bimport\(['"]([^'".\/][^'"]*)['"]\)/g, (_m, dep) => `import("/@modules/${dep}")`);
|
|
492
716
|
// run plugin transforms
|
|
493
717
|
for (const p of plugins) {
|
|
494
718
|
if (p.onTransform) {
|
|
@@ -497,17 +721,28 @@ const overlayId = "__rc_error_overlay__";
|
|
|
497
721
|
code = out;
|
|
498
722
|
}
|
|
499
723
|
}
|
|
500
|
-
// choose loader by extension
|
|
501
724
|
const ext = path.extname(found).toLowerCase();
|
|
502
725
|
const loader = ext === '.ts' ? 'ts' : ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'js';
|
|
503
726
|
const result = await esbuild.transform(code, {
|
|
504
727
|
loader,
|
|
505
728
|
sourcemap: 'inline',
|
|
506
729
|
target: ['es2020'],
|
|
730
|
+
jsx: 'automatic',
|
|
507
731
|
});
|
|
508
|
-
|
|
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);
|
|
509
744
|
res.setHeader('Content-Type', jsContentType());
|
|
510
|
-
res.end(
|
|
745
|
+
res.end(transformedCode);
|
|
511
746
|
}
|
|
512
747
|
catch (err) {
|
|
513
748
|
const e = err;
|
|
@@ -525,10 +760,25 @@ const overlayId = "__rc_error_overlay__";
|
|
|
525
760
|
return res.end('index.html not found');
|
|
526
761
|
}
|
|
527
762
|
try {
|
|
528
|
-
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
|
|
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
|
+
};
|
|
532
782
|
const ws = new WebSocket("ws://" + location.host);
|
|
533
783
|
ws.onmessage = (e) => {
|
|
534
784
|
const msg = JSON.parse(e.data);
|
|
@@ -536,13 +786,25 @@ const overlayId = "__rc_error_overlay__";
|
|
|
536
786
|
if (msg.type === "error") window.showErrorOverlay?.(msg);
|
|
537
787
|
if (msg.type === "update") {
|
|
538
788
|
window.clearErrorOverlay?.();
|
|
539
|
-
|
|
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
|
+
}
|
|
540
801
|
}
|
|
541
802
|
};
|
|
542
|
-
</script
|
|
543
|
-
|
|
803
|
+
</script>`.trim();
|
|
804
|
+
// Inject preamble at the top of <body>
|
|
805
|
+
const newHtml = html.replace('<body>', `<body>\n${reactRefreshPreamble}`);
|
|
544
806
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
545
|
-
res.end(
|
|
807
|
+
res.end(newHtml);
|
|
546
808
|
}
|
|
547
809
|
catch (err) {
|
|
548
810
|
res.writeHead(500);
|
|
@@ -593,12 +855,21 @@ const overlayId = "__rc_error_overlay__";
|
|
|
593
855
|
}
|
|
594
856
|
});
|
|
595
857
|
// graceful shutdown
|
|
596
|
-
|
|
597
|
-
console.log(chalk.red('\n🛑 Shutting down...'));
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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);
|
|
603
874
|
}
|
|
604
875
|
//# sourceMappingURL=dev.js.map
|