almostnode 0.2.6 → 0.2.7
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/frameworks/code-transforms.d.ts +53 -0
- package/dist/frameworks/code-transforms.d.ts.map +1 -0
- package/dist/frameworks/next-dev-server.d.ts +27 -16
- package/dist/frameworks/next-dev-server.d.ts.map +1 -1
- package/dist/frameworks/vite-dev-server.d.ts +0 -4
- package/dist/frameworks/vite-dev-server.d.ts.map +1 -1
- package/dist/index.cjs +20963 -535
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +19156 -1083
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -1
- package/src/convex-app-demo-entry.ts +2 -0
- package/src/frameworks/code-transforms.ts +577 -0
- package/src/frameworks/next-dev-server.ts +748 -229
- package/src/frameworks/vite-dev-server.ts +2 -61
|
@@ -8,6 +8,13 @@ import { VirtualFS } from '../virtual-fs';
|
|
|
8
8
|
import { Buffer } from '../shims/stream';
|
|
9
9
|
import { simpleHash } from '../utils/hash';
|
|
10
10
|
import { loadTailwindConfig } from './tailwind-config-loader';
|
|
11
|
+
import {
|
|
12
|
+
redirectNpmImports as _redirectNpmImports,
|
|
13
|
+
stripCssImports as _stripCssImports,
|
|
14
|
+
addReactRefresh as _addReactRefresh,
|
|
15
|
+
transformEsmToCjsSimple,
|
|
16
|
+
type CssModuleContext,
|
|
17
|
+
} from './code-transforms';
|
|
11
18
|
|
|
12
19
|
// Check if we're in a real browser environment (not jsdom or Node.js)
|
|
13
20
|
const isBrowser = typeof window !== 'undefined' &&
|
|
@@ -80,6 +87,18 @@ export interface NextDevServerOptions extends DevServerOptions {
|
|
|
80
87
|
env?: Record<string, string>;
|
|
81
88
|
/** Asset prefix for static files (e.g., '/marketing'). Auto-detected from next.config if not specified. */
|
|
82
89
|
assetPrefix?: string;
|
|
90
|
+
/** Base path for the app (e.g., '/docs'). Auto-detected from next.config if not specified. */
|
|
91
|
+
basePath?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Resolved App Router route with page, layouts, and UI convention files */
|
|
95
|
+
interface AppRoute {
|
|
96
|
+
page: string;
|
|
97
|
+
layouts: string[];
|
|
98
|
+
params: Record<string, string | string[]>;
|
|
99
|
+
loading?: string;
|
|
100
|
+
error?: string;
|
|
101
|
+
notFound?: string;
|
|
83
102
|
}
|
|
84
103
|
|
|
85
104
|
/**
|
|
@@ -272,11 +291,19 @@ const getVirtualBasePath = () => {
|
|
|
272
291
|
return match[0].endsWith('/') ? match[0] : match[0] + '/';
|
|
273
292
|
};
|
|
274
293
|
|
|
294
|
+
const getBasePath = () => window.__NEXT_BASE_PATH__ || '';
|
|
295
|
+
|
|
275
296
|
const applyVirtualBase = (url) => {
|
|
276
297
|
if (typeof url !== 'string') return url;
|
|
277
298
|
if (!url || url.startsWith('#') || url.startsWith('?')) return url;
|
|
278
299
|
if (/^(https?:)?\\/\\//.test(url)) return url;
|
|
279
300
|
|
|
301
|
+
// Apply basePath first
|
|
302
|
+
const bp = getBasePath();
|
|
303
|
+
if (bp && url.startsWith('/') && !url.startsWith(bp + '/') && url !== bp) {
|
|
304
|
+
url = bp + url;
|
|
305
|
+
}
|
|
306
|
+
|
|
280
307
|
const base = getVirtualBasePath();
|
|
281
308
|
if (!base) return url;
|
|
282
309
|
if (url.startsWith(base)) return url;
|
|
@@ -575,14 +602,47 @@ export function useSearchParams() {
|
|
|
575
602
|
* For route /users/[id]/page.jsx with URL /users/123:
|
|
576
603
|
* @example const { id } = useParams(); // { id: '123' }
|
|
577
604
|
*
|
|
578
|
-
*
|
|
579
|
-
* Full implementation would need route pattern matching.
|
|
605
|
+
* Fetches params from the server's route-info endpoint for dynamic routes.
|
|
580
606
|
*/
|
|
581
607
|
export function useParams() {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
608
|
+
const [params, setParams] = useState(() => {
|
|
609
|
+
// Check if initial params were embedded by the server
|
|
610
|
+
if (typeof window !== 'undefined' && window.__NEXT_ROUTE_PARAMS__) {
|
|
611
|
+
return window.__NEXT_ROUTE_PARAMS__;
|
|
612
|
+
}
|
|
613
|
+
return {};
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
useEffect(() => {
|
|
617
|
+
let cancelled = false;
|
|
618
|
+
|
|
619
|
+
const fetchParams = async () => {
|
|
620
|
+
const pathname = stripVirtualBase(window.location.pathname);
|
|
621
|
+
const base = getVirtualBasePath();
|
|
622
|
+
const baseUrl = base ? base.replace(/\\/$/, '') : '';
|
|
623
|
+
|
|
624
|
+
try {
|
|
625
|
+
const response = await fetch(baseUrl + '/_next/route-info?pathname=' + encodeURIComponent(pathname));
|
|
626
|
+
const info = await response.json();
|
|
627
|
+
if (!cancelled && info.params) {
|
|
628
|
+
setParams(info.params);
|
|
629
|
+
}
|
|
630
|
+
} catch (e) {
|
|
631
|
+
// Silently fail - static routes won't have params
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
fetchParams();
|
|
636
|
+
|
|
637
|
+
const handler = () => fetchParams();
|
|
638
|
+
window.addEventListener('popstate', handler);
|
|
639
|
+
return () => {
|
|
640
|
+
cancelled = true;
|
|
641
|
+
window.removeEventListener('popstate', handler);
|
|
642
|
+
};
|
|
643
|
+
}, []);
|
|
644
|
+
|
|
645
|
+
return params;
|
|
586
646
|
}
|
|
587
647
|
|
|
588
648
|
/**
|
|
@@ -996,6 +1056,96 @@ export const {
|
|
|
996
1056
|
} = fontProxy;
|
|
997
1057
|
`;
|
|
998
1058
|
|
|
1059
|
+
/**
|
|
1060
|
+
* next/font/local shim - Loads local font files
|
|
1061
|
+
* Accepts font source path and creates @font-face declaration + CSS variable
|
|
1062
|
+
*/
|
|
1063
|
+
const NEXT_FONT_LOCAL_SHIM = `
|
|
1064
|
+
const loadedLocalFonts = new Set();
|
|
1065
|
+
|
|
1066
|
+
function localFont(options = {}) {
|
|
1067
|
+
const {
|
|
1068
|
+
src,
|
|
1069
|
+
weight,
|
|
1070
|
+
style = 'normal',
|
|
1071
|
+
variable,
|
|
1072
|
+
display = 'swap',
|
|
1073
|
+
fallback = ['sans-serif'],
|
|
1074
|
+
declarations = [],
|
|
1075
|
+
adjustFontFallback = true
|
|
1076
|
+
} = options;
|
|
1077
|
+
|
|
1078
|
+
// Determine font family name from variable or src
|
|
1079
|
+
const familyName = variable
|
|
1080
|
+
? variable.replace('--', '').replace(/-/g, ' ')
|
|
1081
|
+
: 'local-font-' + Math.random().toString(36).slice(2, 8);
|
|
1082
|
+
|
|
1083
|
+
const fontKey = familyName + '-' + (variable || 'default');
|
|
1084
|
+
if (typeof document !== 'undefined' && !loadedLocalFonts.has(fontKey)) {
|
|
1085
|
+
loadedLocalFonts.add(fontKey);
|
|
1086
|
+
|
|
1087
|
+
// Build @font-face declarations
|
|
1088
|
+
let fontFaces = '';
|
|
1089
|
+
|
|
1090
|
+
if (typeof src === 'string') {
|
|
1091
|
+
// Single source
|
|
1092
|
+
fontFaces = '@font-face {\\n' +
|
|
1093
|
+
' font-family: "' + familyName + '";\\n' +
|
|
1094
|
+
' src: url("' + src + '");\\n' +
|
|
1095
|
+
' font-weight: ' + (weight || '400') + ';\\n' +
|
|
1096
|
+
' font-style: ' + style + ';\\n' +
|
|
1097
|
+
' font-display: ' + display + ';\\n' +
|
|
1098
|
+
'}';
|
|
1099
|
+
} else if (Array.isArray(src)) {
|
|
1100
|
+
// Multiple sources (different weights/styles)
|
|
1101
|
+
fontFaces = src.map(function(s) {
|
|
1102
|
+
const path = typeof s === 'string' ? s : s.path;
|
|
1103
|
+
const w = (typeof s === 'object' && s.weight) || weight || '400';
|
|
1104
|
+
const st = (typeof s === 'object' && s.style) || style;
|
|
1105
|
+
return '@font-face {\\n' +
|
|
1106
|
+
' font-family: "' + familyName + '";\\n' +
|
|
1107
|
+
' src: url("' + path + '");\\n' +
|
|
1108
|
+
' font-weight: ' + w + ';\\n' +
|
|
1109
|
+
' font-style: ' + st + ';\\n' +
|
|
1110
|
+
' font-display: ' + display + ';\\n' +
|
|
1111
|
+
'}';
|
|
1112
|
+
}).join('\\n');
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Inject font-face CSS
|
|
1116
|
+
if (fontFaces) {
|
|
1117
|
+
var styleEl = document.createElement('style');
|
|
1118
|
+
styleEl.setAttribute('data-local-font', fontKey);
|
|
1119
|
+
styleEl.textContent = fontFaces;
|
|
1120
|
+
document.head.appendChild(styleEl);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Inject CSS variable at :root level
|
|
1124
|
+
if (variable) {
|
|
1125
|
+
var varStyle = document.createElement('style');
|
|
1126
|
+
varStyle.setAttribute('data-font-var', variable);
|
|
1127
|
+
varStyle.textContent = ':root { ' + variable + ': "' + familyName + '", ' + fallback.join(', ') + '; }';
|
|
1128
|
+
document.head.appendChild(varStyle);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const className = variable
|
|
1133
|
+
? variable.replace('--', '__')
|
|
1134
|
+
: '__font-' + familyName.toLowerCase().replace(/\\s+/g, '-');
|
|
1135
|
+
|
|
1136
|
+
return {
|
|
1137
|
+
className,
|
|
1138
|
+
variable: className,
|
|
1139
|
+
style: {
|
|
1140
|
+
fontFamily: '"' + familyName + '", ' + fallback.join(', ')
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
export default localFont;
|
|
1146
|
+
export { localFont };
|
|
1147
|
+
`;
|
|
1148
|
+
|
|
999
1149
|
/**
|
|
1000
1150
|
* NextDevServer - A lightweight Next.js-compatible development server
|
|
1001
1151
|
*
|
|
@@ -1056,6 +1206,9 @@ export class NextDevServer extends DevServer {
|
|
|
1056
1206
|
/** Asset prefix for static files (e.g., '/marketing') */
|
|
1057
1207
|
private assetPrefix: string = '';
|
|
1058
1208
|
|
|
1209
|
+
/** Base path for the app (e.g., '/docs') */
|
|
1210
|
+
private basePath: string = '';
|
|
1211
|
+
|
|
1059
1212
|
constructor(vfs: VirtualFS, options: NextDevServerOptions) {
|
|
1060
1213
|
super(vfs, options);
|
|
1061
1214
|
this.options = options;
|
|
@@ -1077,6 +1230,9 @@ export class NextDevServer extends DevServer {
|
|
|
1077
1230
|
|
|
1078
1231
|
// Load assetPrefix from options or auto-detect from next.config
|
|
1079
1232
|
this.loadAssetPrefix(options.assetPrefix);
|
|
1233
|
+
|
|
1234
|
+
// Load basePath from options or auto-detect from next.config
|
|
1235
|
+
this.loadBasePath(options.basePath);
|
|
1080
1236
|
}
|
|
1081
1237
|
|
|
1082
1238
|
/**
|
|
@@ -1160,6 +1316,38 @@ export class NextDevServer extends DevServer {
|
|
|
1160
1316
|
}
|
|
1161
1317
|
}
|
|
1162
1318
|
|
|
1319
|
+
/**
|
|
1320
|
+
* Load basePath from options or auto-detect from next.config.ts/js
|
|
1321
|
+
* The basePath is used to prefix all routes (e.g., '/docs' means / -> /docs)
|
|
1322
|
+
*/
|
|
1323
|
+
private loadBasePath(optionValue?: string): void {
|
|
1324
|
+
if (optionValue !== undefined) {
|
|
1325
|
+
this.basePath = optionValue.startsWith('/') ? optionValue : `/${optionValue}`;
|
|
1326
|
+
if (this.basePath.endsWith('/')) {
|
|
1327
|
+
this.basePath = this.basePath.slice(0, -1);
|
|
1328
|
+
}
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
try {
|
|
1333
|
+
const configFiles = ['/next.config.ts', '/next.config.js', '/next.config.mjs'];
|
|
1334
|
+
for (const configPath of configFiles) {
|
|
1335
|
+
if (!this.vfs.existsSync(configPath)) continue;
|
|
1336
|
+
const content = this.vfs.readFileSync(configPath, 'utf-8');
|
|
1337
|
+
const match = content.match(/basePath\s*:\s*["']([^"']+)["']/);
|
|
1338
|
+
if (match) {
|
|
1339
|
+
let bp = match[1];
|
|
1340
|
+
if (!bp.startsWith('/')) bp = `/${bp}`;
|
|
1341
|
+
if (bp.endsWith('/')) bp = bp.slice(0, -1);
|
|
1342
|
+
this.basePath = bp;
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
} catch {
|
|
1347
|
+
// Silently ignore config parse errors
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1163
1351
|
/**
|
|
1164
1352
|
* Resolve path aliases in transformed code
|
|
1165
1353
|
* Converts imports like "@/components/foo" to "/__virtual__/PORT/components/foo"
|
|
@@ -1246,6 +1434,8 @@ export class NextDevServer extends DevServer {
|
|
|
1246
1434
|
window.process = window.process || {};
|
|
1247
1435
|
window.process.env = window.process.env || {};
|
|
1248
1436
|
Object.assign(window.process.env, ${JSON.stringify(publicEnvVars)});
|
|
1437
|
+
// Next.js config values
|
|
1438
|
+
window.__NEXT_BASE_PATH__ = ${JSON.stringify(this.basePath)};
|
|
1249
1439
|
</script>`;
|
|
1250
1440
|
}
|
|
1251
1441
|
|
|
@@ -1285,11 +1475,30 @@ export class NextDevServer extends DevServer {
|
|
|
1285
1475
|
// Check if /app directory exists and has a page file
|
|
1286
1476
|
if (!this.exists(this.appDir)) return false;
|
|
1287
1477
|
|
|
1288
|
-
// Check for root page
|
|
1289
1478
|
const extensions = ['.jsx', '.tsx', '.js', '.ts'];
|
|
1479
|
+
|
|
1480
|
+
// Check for root page directly
|
|
1290
1481
|
for (const ext of extensions) {
|
|
1291
1482
|
if (this.exists(`${this.appDir}/page${ext}`)) return true;
|
|
1292
1483
|
}
|
|
1484
|
+
|
|
1485
|
+
// Check for root page inside route groups (e.g., /app/(main)/page.tsx)
|
|
1486
|
+
try {
|
|
1487
|
+
const entries = this.vfs.readdirSync(this.appDir);
|
|
1488
|
+
for (const entry of entries) {
|
|
1489
|
+
if (/^\([^)]+\)$/.test(entry) && this.isDirectory(`${this.appDir}/${entry}`)) {
|
|
1490
|
+
for (const ext of extensions) {
|
|
1491
|
+
if (this.exists(`${this.appDir}/${entry}/page${ext}`)) return true;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
} catch { /* ignore */ }
|
|
1496
|
+
|
|
1497
|
+
// Also check for any layout.tsx which indicates App Router usage
|
|
1498
|
+
for (const ext of extensions) {
|
|
1499
|
+
if (this.exists(`${this.appDir}/layout${ext}`)) return true;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1293
1502
|
return false;
|
|
1294
1503
|
} catch {
|
|
1295
1504
|
return false;
|
|
@@ -1329,6 +1538,14 @@ export class NextDevServer extends DevServer {
|
|
|
1329
1538
|
}
|
|
1330
1539
|
}
|
|
1331
1540
|
|
|
1541
|
+
// Strip basePath if present (e.g., /docs/about -> /about)
|
|
1542
|
+
if (this.basePath && pathname.startsWith(this.basePath)) {
|
|
1543
|
+
const rest = pathname.slice(this.basePath.length);
|
|
1544
|
+
if (rest === '' || rest.startsWith('/')) {
|
|
1545
|
+
pathname = rest || '/';
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1332
1549
|
// Serve Next.js shims
|
|
1333
1550
|
if (pathname.startsWith('/_next/shims/')) {
|
|
1334
1551
|
return this.serveNextShim(pathname);
|
|
@@ -1354,7 +1571,15 @@ export class NextDevServer extends DevServer {
|
|
|
1354
1571
|
return this.serveStaticAsset(pathname);
|
|
1355
1572
|
}
|
|
1356
1573
|
|
|
1357
|
-
// API routes
|
|
1574
|
+
// App Router API routes (route.ts/route.js) - check before Pages Router API routes
|
|
1575
|
+
if (this.useAppRouter) {
|
|
1576
|
+
const appRouteFile = this.resolveAppRouteHandler(pathname);
|
|
1577
|
+
if (appRouteFile) {
|
|
1578
|
+
return this.handleAppRouteHandler(method, pathname, headers, body, appRouteFile, urlObj.search);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// Pages Router API routes: /api/*
|
|
1358
1583
|
if (pathname.startsWith('/api/')) {
|
|
1359
1584
|
return this.handleApiRoute(method, pathname, headers, body);
|
|
1360
1585
|
}
|
|
@@ -1421,6 +1646,9 @@ export class NextDevServer extends DevServer {
|
|
|
1421
1646
|
case 'font/google':
|
|
1422
1647
|
code = NEXT_FONT_GOOGLE_SHIM;
|
|
1423
1648
|
break;
|
|
1649
|
+
case 'font/local':
|
|
1650
|
+
code = NEXT_FONT_LOCAL_SHIM;
|
|
1651
|
+
break;
|
|
1424
1652
|
default:
|
|
1425
1653
|
return this.notFound(pathname);
|
|
1426
1654
|
}
|
|
@@ -1576,6 +1804,238 @@ export class NextDevServer extends DevServer {
|
|
|
1576
1804
|
}
|
|
1577
1805
|
}
|
|
1578
1806
|
|
|
1807
|
+
/**
|
|
1808
|
+
* Resolve an App Router route handler (route.ts/route.js)
|
|
1809
|
+
* Returns the file path if found, null otherwise
|
|
1810
|
+
*/
|
|
1811
|
+
private resolveAppRouteHandler(pathname: string): string | null {
|
|
1812
|
+
const extensions = ['.ts', '.js', '.tsx', '.jsx'];
|
|
1813
|
+
|
|
1814
|
+
// Build the directory path in the app dir
|
|
1815
|
+
const segments = pathname === '/' ? [] : pathname.split('/').filter(Boolean);
|
|
1816
|
+
let dirPath = this.appDir;
|
|
1817
|
+
|
|
1818
|
+
for (const segment of segments) {
|
|
1819
|
+
dirPath = `${dirPath}/${segment}`;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// Check for route file
|
|
1823
|
+
for (const ext of extensions) {
|
|
1824
|
+
const routePath = `${dirPath}/route${ext}`;
|
|
1825
|
+
if (this.exists(routePath)) {
|
|
1826
|
+
return routePath;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// Try dynamic route resolution with route groups
|
|
1831
|
+
return this.resolveAppRouteHandlerDynamic(segments);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
/**
|
|
1835
|
+
* Resolve dynamic App Router route handlers with route group support
|
|
1836
|
+
*/
|
|
1837
|
+
private resolveAppRouteHandlerDynamic(segments: string[]): string | null {
|
|
1838
|
+
const extensions = ['.ts', '.js', '.tsx', '.jsx'];
|
|
1839
|
+
|
|
1840
|
+
const tryPath = (dirPath: string, remainingSegments: string[]): string | null => {
|
|
1841
|
+
if (remainingSegments.length === 0) {
|
|
1842
|
+
for (const ext of extensions) {
|
|
1843
|
+
const routePath = `${dirPath}/route${ext}`;
|
|
1844
|
+
if (this.exists(routePath)) {
|
|
1845
|
+
return routePath;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// Check route groups
|
|
1850
|
+
try {
|
|
1851
|
+
const entries = this.vfs.readdirSync(dirPath);
|
|
1852
|
+
for (const entry of entries) {
|
|
1853
|
+
if (/^\([^)]+\)$/.test(entry) && this.isDirectory(`${dirPath}/${entry}`)) {
|
|
1854
|
+
for (const ext of extensions) {
|
|
1855
|
+
const routePath = `${dirPath}/${entry}/route${ext}`;
|
|
1856
|
+
if (this.exists(routePath)) {
|
|
1857
|
+
return routePath;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
} catch { /* ignore */ }
|
|
1863
|
+
|
|
1864
|
+
return null;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
const [current, ...rest] = remainingSegments;
|
|
1868
|
+
|
|
1869
|
+
// Try exact match
|
|
1870
|
+
const exactPath = `${dirPath}/${current}`;
|
|
1871
|
+
if (this.isDirectory(exactPath)) {
|
|
1872
|
+
const result = tryPath(exactPath, rest);
|
|
1873
|
+
if (result) return result;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// Try route groups and dynamic segments
|
|
1877
|
+
try {
|
|
1878
|
+
const entries = this.vfs.readdirSync(dirPath);
|
|
1879
|
+
for (const entry of entries) {
|
|
1880
|
+
// Route groups
|
|
1881
|
+
if (/^\([^)]+\)$/.test(entry) && this.isDirectory(`${dirPath}/${entry}`)) {
|
|
1882
|
+
const groupExact = `${dirPath}/${entry}/${current}`;
|
|
1883
|
+
if (this.isDirectory(groupExact)) {
|
|
1884
|
+
const result = tryPath(groupExact, rest);
|
|
1885
|
+
if (result) return result;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
// Dynamic segments
|
|
1889
|
+
if (entry.startsWith('[') && entry.endsWith(']') && !entry.includes('.')) {
|
|
1890
|
+
const dynamicPath = `${dirPath}/${entry}`;
|
|
1891
|
+
if (this.isDirectory(dynamicPath)) {
|
|
1892
|
+
const result = tryPath(dynamicPath, rest);
|
|
1893
|
+
if (result) return result;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
// Catch-all
|
|
1897
|
+
if (entry.startsWith('[...') && entry.endsWith(']')) {
|
|
1898
|
+
const dynamicPath = `${dirPath}/${entry}`;
|
|
1899
|
+
if (this.isDirectory(dynamicPath)) {
|
|
1900
|
+
const result = tryPath(dynamicPath, []);
|
|
1901
|
+
if (result) return result;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
} catch { /* ignore */ }
|
|
1906
|
+
|
|
1907
|
+
return null;
|
|
1908
|
+
};
|
|
1909
|
+
|
|
1910
|
+
return tryPath(this.appDir, segments);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
/**
|
|
1914
|
+
* Handle App Router route handler (route.ts) requests
|
|
1915
|
+
* These use the Web Request/Response API pattern
|
|
1916
|
+
*/
|
|
1917
|
+
private async handleAppRouteHandler(
|
|
1918
|
+
method: string,
|
|
1919
|
+
pathname: string,
|
|
1920
|
+
headers: Record<string, string>,
|
|
1921
|
+
body: Buffer | undefined,
|
|
1922
|
+
routeFile: string,
|
|
1923
|
+
search?: string
|
|
1924
|
+
): Promise<ResponseData> {
|
|
1925
|
+
try {
|
|
1926
|
+
const code = this.vfs.readFileSync(routeFile, 'utf8');
|
|
1927
|
+
const transformed = await this.transformApiHandler(code, routeFile);
|
|
1928
|
+
|
|
1929
|
+
// Create module context
|
|
1930
|
+
const builtinModules: Record<string, unknown> = {
|
|
1931
|
+
https: await import('../shims/https'),
|
|
1932
|
+
http: await import('../shims/http'),
|
|
1933
|
+
path: await import('../shims/path'),
|
|
1934
|
+
url: await import('../shims/url'),
|
|
1935
|
+
querystring: await import('../shims/querystring'),
|
|
1936
|
+
util: await import('../shims/util'),
|
|
1937
|
+
events: await import('../shims/events'),
|
|
1938
|
+
stream: await import('../shims/stream'),
|
|
1939
|
+
buffer: await import('../shims/buffer'),
|
|
1940
|
+
crypto: await import('../shims/crypto'),
|
|
1941
|
+
};
|
|
1942
|
+
|
|
1943
|
+
const require = (id: string): unknown => {
|
|
1944
|
+
const modId = id.startsWith('node:') ? id.slice(5) : id;
|
|
1945
|
+
if (builtinModules[modId]) return builtinModules[modId];
|
|
1946
|
+
throw new Error(`Module not found: ${id}`);
|
|
1947
|
+
};
|
|
1948
|
+
|
|
1949
|
+
const module = { exports: {} as Record<string, unknown> };
|
|
1950
|
+
const exports = module.exports;
|
|
1951
|
+
const process = {
|
|
1952
|
+
env: { ...this.options.env },
|
|
1953
|
+
cwd: () => '/',
|
|
1954
|
+
platform: 'browser',
|
|
1955
|
+
version: 'v18.0.0',
|
|
1956
|
+
versions: { node: '18.0.0' },
|
|
1957
|
+
};
|
|
1958
|
+
|
|
1959
|
+
const fn = new Function('exports', 'require', 'module', 'process', transformed);
|
|
1960
|
+
fn(exports, require, module, process);
|
|
1961
|
+
|
|
1962
|
+
// Get the handler for the HTTP method
|
|
1963
|
+
const methodUpper = method.toUpperCase();
|
|
1964
|
+
const handler = module.exports[methodUpper] || module.exports[methodUpper.toLowerCase()];
|
|
1965
|
+
|
|
1966
|
+
if (typeof handler !== 'function') {
|
|
1967
|
+
return {
|
|
1968
|
+
statusCode: 405,
|
|
1969
|
+
statusMessage: 'Method Not Allowed',
|
|
1970
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
1971
|
+
body: Buffer.from(JSON.stringify({ error: `Method ${method} not allowed` })),
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// Create a Web API Request object
|
|
1976
|
+
const requestUrl = new URL(pathname + (search || ''), 'http://localhost');
|
|
1977
|
+
const requestInit: RequestInit = {
|
|
1978
|
+
method: methodUpper,
|
|
1979
|
+
headers: new Headers(headers),
|
|
1980
|
+
};
|
|
1981
|
+
if (body && methodUpper !== 'GET' && methodUpper !== 'HEAD') {
|
|
1982
|
+
requestInit.body = body;
|
|
1983
|
+
}
|
|
1984
|
+
const request = new Request(requestUrl.toString(), requestInit);
|
|
1985
|
+
|
|
1986
|
+
// Extract route params
|
|
1987
|
+
const route = this.resolveAppRoute(pathname);
|
|
1988
|
+
const params = route?.params || {};
|
|
1989
|
+
|
|
1990
|
+
// Call the handler
|
|
1991
|
+
const response = await handler(request, { params: Promise.resolve(params) });
|
|
1992
|
+
|
|
1993
|
+
// Convert Response to our format
|
|
1994
|
+
if (response instanceof Response) {
|
|
1995
|
+
const respHeaders: Record<string, string> = {};
|
|
1996
|
+
response.headers.forEach((value: string, key: string) => {
|
|
1997
|
+
respHeaders[key] = value;
|
|
1998
|
+
});
|
|
1999
|
+
|
|
2000
|
+
const respBody = await response.text();
|
|
2001
|
+
return {
|
|
2002
|
+
statusCode: response.status,
|
|
2003
|
+
statusMessage: response.statusText || 'OK',
|
|
2004
|
+
headers: respHeaders,
|
|
2005
|
+
body: Buffer.from(respBody),
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// If the handler returned a plain object, serialize as JSON
|
|
2010
|
+
if (response && typeof response === 'object') {
|
|
2011
|
+
const json = JSON.stringify(response);
|
|
2012
|
+
return {
|
|
2013
|
+
statusCode: 200,
|
|
2014
|
+
statusMessage: 'OK',
|
|
2015
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
2016
|
+
body: Buffer.from(json),
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
return {
|
|
2021
|
+
statusCode: 200,
|
|
2022
|
+
statusMessage: 'OK',
|
|
2023
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
2024
|
+
body: Buffer.from(String(response || '')),
|
|
2025
|
+
};
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
console.error('[NextDevServer] App Route handler error:', error);
|
|
2028
|
+
return {
|
|
2029
|
+
statusCode: 500,
|
|
2030
|
+
statusMessage: 'Internal Server Error',
|
|
2031
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
2032
|
+
body: Buffer.from(JSON.stringify({
|
|
2033
|
+
error: error instanceof Error ? error.message : 'Internal Server Error'
|
|
2034
|
+
})),
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
|
|
1579
2039
|
/**
|
|
1580
2040
|
* Handle streaming API route requests
|
|
1581
2041
|
* This is called by the server bridge for requests that need streaming support
|
|
@@ -2077,82 +2537,128 @@ export class NextDevServer extends DevServer {
|
|
|
2077
2537
|
/**
|
|
2078
2538
|
* Resolve App Router route to page and layout files
|
|
2079
2539
|
*/
|
|
2080
|
-
private resolveAppRoute(pathname: string):
|
|
2081
|
-
const extensions = ['.jsx', '.tsx', '.js', '.ts'];
|
|
2540
|
+
private resolveAppRoute(pathname: string): AppRoute | null {
|
|
2082
2541
|
const segments = pathname === '/' ? [] : pathname.split('/').filter(Boolean);
|
|
2542
|
+
// Use the unified dynamic resolver which handles static, dynamic, and route groups
|
|
2543
|
+
return this.resolveAppDynamicRoute(pathname, segments);
|
|
2544
|
+
}
|
|
2083
2545
|
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2546
|
+
/**
|
|
2547
|
+
* Resolve App Router routes including static, dynamic, and route groups.
|
|
2548
|
+
* Route groups are folders wrapped in parentheses like (marketing) that
|
|
2549
|
+
* don't affect the URL path but can have their own layouts.
|
|
2550
|
+
*/
|
|
2551
|
+
private resolveAppDynamicRoute(
|
|
2552
|
+
_pathname: string,
|
|
2553
|
+
segments: string[]
|
|
2554
|
+
): AppRoute | null {
|
|
2555
|
+
const extensions = ['.jsx', '.tsx', '.js', '.ts'];
|
|
2087
2556
|
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2557
|
+
/**
|
|
2558
|
+
* Collect layout from a directory if it exists
|
|
2559
|
+
*/
|
|
2560
|
+
const collectLayout = (dirPath: string, layouts: string[]): string[] => {
|
|
2561
|
+
for (const ext of extensions) {
|
|
2562
|
+
const layoutPath = `${dirPath}/layout${ext}`;
|
|
2563
|
+
if (this.exists(layoutPath) && !layouts.includes(layoutPath)) {
|
|
2564
|
+
return [...layouts, layoutPath];
|
|
2565
|
+
}
|
|
2094
2566
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
// Walk through segments to find page and collect layouts
|
|
2098
|
-
for (const segment of segments) {
|
|
2099
|
-
dirPath = `${dirPath}/${segment}`;
|
|
2567
|
+
return layouts;
|
|
2568
|
+
};
|
|
2100
2569
|
|
|
2101
|
-
|
|
2570
|
+
/**
|
|
2571
|
+
* Find page file in a directory
|
|
2572
|
+
*/
|
|
2573
|
+
const findPage = (dirPath: string): string | null => {
|
|
2102
2574
|
for (const ext of extensions) {
|
|
2103
|
-
const
|
|
2104
|
-
if (this.exists(
|
|
2105
|
-
|
|
2106
|
-
break;
|
|
2575
|
+
const pagePath = `${dirPath}/page${ext}`;
|
|
2576
|
+
if (this.exists(pagePath)) {
|
|
2577
|
+
return pagePath;
|
|
2107
2578
|
}
|
|
2108
2579
|
}
|
|
2109
|
-
|
|
2580
|
+
return null;
|
|
2581
|
+
};
|
|
2110
2582
|
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2583
|
+
/**
|
|
2584
|
+
* Find a UI convention file (loading, error, not-found) in a directory
|
|
2585
|
+
*/
|
|
2586
|
+
const findConventionFile = (dirPath: string, name: string): string | null => {
|
|
2587
|
+
for (const ext of extensions) {
|
|
2588
|
+
const filePath = `${dirPath}/${name}${ext}`;
|
|
2589
|
+
if (this.exists(filePath)) {
|
|
2590
|
+
return filePath;
|
|
2591
|
+
}
|
|
2117
2592
|
}
|
|
2118
|
-
|
|
2593
|
+
return null;
|
|
2594
|
+
};
|
|
2119
2595
|
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2596
|
+
/**
|
|
2597
|
+
* Find the nearest convention file by walking up from the page directory
|
|
2598
|
+
*/
|
|
2599
|
+
const findNearestConventionFile = (dirPath: string, name: string): string | null => {
|
|
2600
|
+
let current = dirPath;
|
|
2601
|
+
while (current.startsWith(this.appDir)) {
|
|
2602
|
+
const file = findConventionFile(current, name);
|
|
2603
|
+
if (file) return file;
|
|
2604
|
+
// Move up one directory
|
|
2605
|
+
const parent = current.replace(/\/[^/]+$/, '');
|
|
2606
|
+
if (parent === current) break;
|
|
2607
|
+
current = parent;
|
|
2608
|
+
}
|
|
2609
|
+
return null;
|
|
2610
|
+
};
|
|
2123
2611
|
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2612
|
+
/**
|
|
2613
|
+
* Get route group directories (folders matching (name) pattern)
|
|
2614
|
+
*/
|
|
2615
|
+
const getRouteGroups = (dirPath: string): string[] => {
|
|
2616
|
+
try {
|
|
2617
|
+
const entries = this.vfs.readdirSync(dirPath);
|
|
2618
|
+
return entries.filter(e => /^\([^)]+\)$/.test(e) && this.isDirectory(`${dirPath}/${e}`));
|
|
2619
|
+
} catch {
|
|
2620
|
+
return [];
|
|
2621
|
+
}
|
|
2622
|
+
};
|
|
2133
2623
|
|
|
2134
2624
|
const tryPath = (
|
|
2135
2625
|
dirPath: string,
|
|
2136
2626
|
remainingSegments: string[],
|
|
2137
2627
|
layouts: string[],
|
|
2138
2628
|
params: Record<string, string | string[]>
|
|
2139
|
-
):
|
|
2629
|
+
): AppRoute | null => {
|
|
2140
2630
|
// Check for layout at current level
|
|
2141
|
-
|
|
2142
|
-
const layoutPath = `${dirPath}/layout${ext}`;
|
|
2143
|
-
if (this.exists(layoutPath) && !layouts.includes(layoutPath)) {
|
|
2144
|
-
layouts = [...layouts, layoutPath];
|
|
2145
|
-
}
|
|
2146
|
-
}
|
|
2631
|
+
layouts = collectLayout(dirPath, layouts);
|
|
2147
2632
|
|
|
2148
2633
|
if (remainingSegments.length === 0) {
|
|
2149
|
-
// Look for page file
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2634
|
+
// Look for page file directly
|
|
2635
|
+
const page = findPage(dirPath);
|
|
2636
|
+
if (page) {
|
|
2637
|
+
return {
|
|
2638
|
+
page, layouts, params,
|
|
2639
|
+
loading: findNearestConventionFile(dirPath, 'loading') || undefined,
|
|
2640
|
+
error: findNearestConventionFile(dirPath, 'error') || undefined,
|
|
2641
|
+
notFound: findNearestConventionFile(dirPath, 'not-found') || undefined,
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
// Look for page inside route groups at this level
|
|
2646
|
+
// e.g., /app/(marketing)/page.tsx resolves to /
|
|
2647
|
+
const groups = getRouteGroups(dirPath);
|
|
2648
|
+
for (const group of groups) {
|
|
2649
|
+
const groupPath = `${dirPath}/${group}`;
|
|
2650
|
+
const groupLayouts = collectLayout(groupPath, layouts);
|
|
2651
|
+
const page = findPage(groupPath);
|
|
2652
|
+
if (page) {
|
|
2653
|
+
return {
|
|
2654
|
+
page, layouts: groupLayouts, params,
|
|
2655
|
+
loading: findNearestConventionFile(groupPath, 'loading') || undefined,
|
|
2656
|
+
error: findNearestConventionFile(groupPath, 'error') || undefined,
|
|
2657
|
+
notFound: findNearestConventionFile(groupPath, 'not-found') || undefined,
|
|
2658
|
+
};
|
|
2154
2659
|
}
|
|
2155
2660
|
}
|
|
2661
|
+
|
|
2156
2662
|
return null;
|
|
2157
2663
|
}
|
|
2158
2664
|
|
|
@@ -2165,7 +2671,56 @@ export class NextDevServer extends DevServer {
|
|
|
2165
2671
|
if (result) return result;
|
|
2166
2672
|
}
|
|
2167
2673
|
|
|
2168
|
-
// Try
|
|
2674
|
+
// Try inside route groups - route groups are transparent in URL
|
|
2675
|
+
// e.g., /about might match /app/(marketing)/about/page.tsx
|
|
2676
|
+
const groups = getRouteGroups(dirPath);
|
|
2677
|
+
for (const group of groups) {
|
|
2678
|
+
const groupPath = `${dirPath}/${group}`;
|
|
2679
|
+
const groupLayouts = collectLayout(groupPath, layouts);
|
|
2680
|
+
|
|
2681
|
+
// Try exact match inside group
|
|
2682
|
+
const groupExactPath = `${groupPath}/${current}`;
|
|
2683
|
+
if (this.isDirectory(groupExactPath)) {
|
|
2684
|
+
const result = tryPath(groupExactPath, rest, groupLayouts, params);
|
|
2685
|
+
if (result) return result;
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
// Try dynamic segments inside group
|
|
2689
|
+
try {
|
|
2690
|
+
const groupEntries = this.vfs.readdirSync(groupPath);
|
|
2691
|
+
for (const entry of groupEntries) {
|
|
2692
|
+
if (entry.startsWith('[...') && entry.endsWith(']')) {
|
|
2693
|
+
const dynamicPath = `${groupPath}/${entry}`;
|
|
2694
|
+
if (this.isDirectory(dynamicPath)) {
|
|
2695
|
+
const paramName = entry.slice(4, -1);
|
|
2696
|
+
const newParams = { ...params, [paramName]: [current, ...rest] };
|
|
2697
|
+
const result = tryPath(dynamicPath, [], groupLayouts, newParams);
|
|
2698
|
+
if (result) return result;
|
|
2699
|
+
}
|
|
2700
|
+
} else if (entry.startsWith('[[...') && entry.endsWith(']]')) {
|
|
2701
|
+
const dynamicPath = `${groupPath}/${entry}`;
|
|
2702
|
+
if (this.isDirectory(dynamicPath)) {
|
|
2703
|
+
const paramName = entry.slice(5, -2);
|
|
2704
|
+
const newParams = { ...params, [paramName]: [current, ...rest] };
|
|
2705
|
+
const result = tryPath(dynamicPath, [], groupLayouts, newParams);
|
|
2706
|
+
if (result) return result;
|
|
2707
|
+
}
|
|
2708
|
+
} else if (entry.startsWith('[') && entry.endsWith(']') && !entry.includes('.')) {
|
|
2709
|
+
const dynamicPath = `${groupPath}/${entry}`;
|
|
2710
|
+
if (this.isDirectory(dynamicPath)) {
|
|
2711
|
+
const paramName = entry.slice(1, -1);
|
|
2712
|
+
const newParams = { ...params, [paramName]: current };
|
|
2713
|
+
const result = tryPath(dynamicPath, rest, groupLayouts, newParams);
|
|
2714
|
+
if (result) return result;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
} catch {
|
|
2719
|
+
// Group directory read failed
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
// Try dynamic segments at current level
|
|
2169
2724
|
try {
|
|
2170
2725
|
const entries = this.vfs.readdirSync(dirPath);
|
|
2171
2726
|
for (const entry of entries) {
|
|
@@ -2173,9 +2728,17 @@ export class NextDevServer extends DevServer {
|
|
|
2173
2728
|
if (entry.startsWith('[...') && entry.endsWith(']')) {
|
|
2174
2729
|
const dynamicPath = `${dirPath}/${entry}`;
|
|
2175
2730
|
if (this.isDirectory(dynamicPath)) {
|
|
2176
|
-
// Extract param name from [...slug]
|
|
2177
2731
|
const paramName = entry.slice(4, -1);
|
|
2178
|
-
|
|
2732
|
+
const newParams = { ...params, [paramName]: [current, ...rest] };
|
|
2733
|
+
const result = tryPath(dynamicPath, [], layouts, newParams);
|
|
2734
|
+
if (result) return result;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
// Handle optional catch-all routes [[...slug]]
|
|
2738
|
+
else if (entry.startsWith('[[...') && entry.endsWith(']]')) {
|
|
2739
|
+
const dynamicPath = `${dirPath}/${entry}`;
|
|
2740
|
+
if (this.isDirectory(dynamicPath)) {
|
|
2741
|
+
const paramName = entry.slice(5, -2);
|
|
2179
2742
|
const newParams = { ...params, [paramName]: [current, ...rest] };
|
|
2180
2743
|
const result = tryPath(dynamicPath, [], layouts, newParams);
|
|
2181
2744
|
if (result) return result;
|
|
@@ -2185,7 +2748,6 @@ export class NextDevServer extends DevServer {
|
|
|
2185
2748
|
else if (entry.startsWith('[') && entry.endsWith(']') && !entry.includes('.')) {
|
|
2186
2749
|
const dynamicPath = `${dirPath}/${entry}`;
|
|
2187
2750
|
if (this.isDirectory(dynamicPath)) {
|
|
2188
|
-
// Extract param name from [id]
|
|
2189
2751
|
const paramName = entry.slice(1, -1);
|
|
2190
2752
|
const newParams = { ...params, [paramName]: current };
|
|
2191
2753
|
const result = tryPath(dynamicPath, rest, layouts, newParams);
|
|
@@ -2217,7 +2779,7 @@ export class NextDevServer extends DevServer {
|
|
|
2217
2779
|
* Generate HTML for App Router with nested layouts
|
|
2218
2780
|
*/
|
|
2219
2781
|
private async generateAppRouterHtml(
|
|
2220
|
-
route:
|
|
2782
|
+
route: AppRoute,
|
|
2221
2783
|
pathname: string
|
|
2222
2784
|
): Promise<string> {
|
|
2223
2785
|
// Use virtual server prefix for all file imports so the service worker can intercept them
|
|
@@ -2239,6 +2801,11 @@ export class NextDevServer extends DevServer {
|
|
|
2239
2801
|
.map((layout, i) => `import Layout${i} from '${virtualPrefix}${layout}';`)
|
|
2240
2802
|
.join('\n ');
|
|
2241
2803
|
|
|
2804
|
+
// Build convention file paths for the inline script
|
|
2805
|
+
const loadingModulePath = route.loading ? `${virtualPrefix}${route.loading}` : '';
|
|
2806
|
+
const errorModulePath = route.error ? `${virtualPrefix}${route.error}` : '';
|
|
2807
|
+
const notFoundModulePath = route.notFound ? `${virtualPrefix}${route.notFound}` : '';
|
|
2808
|
+
|
|
2242
2809
|
// Build nested JSX: Layout0 > Layout1 > ... > Page
|
|
2243
2810
|
let nestedJsx = 'React.createElement(Page)';
|
|
2244
2811
|
for (let i = route.layouts.length - 1; i >= 0; i--) {
|
|
@@ -2286,7 +2853,8 @@ export class NextDevServer extends DevServer {
|
|
|
2286
2853
|
"next/image": "${virtualPrefix}/_next/shims/image.js",
|
|
2287
2854
|
"next/dynamic": "${virtualPrefix}/_next/shims/dynamic.js",
|
|
2288
2855
|
"next/script": "${virtualPrefix}/_next/shims/script.js",
|
|
2289
|
-
"next/font/google": "${virtualPrefix}/_next/shims/font/google.js"
|
|
2856
|
+
"next/font/google": "${virtualPrefix}/_next/shims/font/google.js",
|
|
2857
|
+
"next/font/local": "${virtualPrefix}/_next/shims/font/local.js"
|
|
2290
2858
|
}
|
|
2291
2859
|
}
|
|
2292
2860
|
</script>
|
|
@@ -2304,6 +2872,14 @@ export class NextDevServer extends DevServer {
|
|
|
2304
2872
|
const initialRouteParams = ${JSON.stringify(route.params)};
|
|
2305
2873
|
const initialPathname = '${pathname}';
|
|
2306
2874
|
|
|
2875
|
+
// Expose initial params for useParams() hook
|
|
2876
|
+
window.__NEXT_ROUTE_PARAMS__ = initialRouteParams;
|
|
2877
|
+
|
|
2878
|
+
// Convention file paths (loading.tsx, error.tsx, not-found.tsx)
|
|
2879
|
+
const loadingModulePath = '${loadingModulePath}';
|
|
2880
|
+
const errorModulePath = '${errorModulePath}';
|
|
2881
|
+
const notFoundModulePath = '${notFoundModulePath}';
|
|
2882
|
+
|
|
2307
2883
|
// Route params cache for client-side navigation
|
|
2308
2884
|
const routeParamsCache = new Map();
|
|
2309
2885
|
routeParamsCache.set(initialPathname, initialRouteParams);
|
|
@@ -2399,10 +2975,66 @@ export class NextDevServer extends DevServer {
|
|
|
2399
2975
|
return layouts;
|
|
2400
2976
|
}
|
|
2401
2977
|
|
|
2978
|
+
// Load convention components (loading.tsx, error.tsx)
|
|
2979
|
+
let LoadingComponent = null;
|
|
2980
|
+
let ErrorComponent = null;
|
|
2981
|
+
let NotFoundComponent = null;
|
|
2982
|
+
|
|
2983
|
+
async function loadConventionComponents() {
|
|
2984
|
+
if (loadingModulePath) {
|
|
2985
|
+
try {
|
|
2986
|
+
const mod = await import(/* @vite-ignore */ loadingModulePath);
|
|
2987
|
+
LoadingComponent = mod.default;
|
|
2988
|
+
} catch (e) { /* loading.tsx not available */ }
|
|
2989
|
+
}
|
|
2990
|
+
if (errorModulePath) {
|
|
2991
|
+
try {
|
|
2992
|
+
const mod = await import(/* @vite-ignore */ errorModulePath);
|
|
2993
|
+
ErrorComponent = mod.default;
|
|
2994
|
+
} catch (e) { /* error.tsx not available */ }
|
|
2995
|
+
}
|
|
2996
|
+
if (notFoundModulePath) {
|
|
2997
|
+
try {
|
|
2998
|
+
const mod = await import(/* @vite-ignore */ notFoundModulePath);
|
|
2999
|
+
NotFoundComponent = mod.default;
|
|
3000
|
+
} catch (e) { /* not-found.tsx not available */ }
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
await loadConventionComponents();
|
|
3004
|
+
|
|
3005
|
+
// Error boundary class component
|
|
3006
|
+
class ErrorBoundary extends React.Component {
|
|
3007
|
+
constructor(props) {
|
|
3008
|
+
super(props);
|
|
3009
|
+
this.state = { error: null };
|
|
3010
|
+
}
|
|
3011
|
+
static getDerivedStateFromError(error) {
|
|
3012
|
+
return { error };
|
|
3013
|
+
}
|
|
3014
|
+
componentDidCatch(error, info) {
|
|
3015
|
+
console.error('[ErrorBoundary]', error, info);
|
|
3016
|
+
}
|
|
3017
|
+
render() {
|
|
3018
|
+
if (this.state.error) {
|
|
3019
|
+
if (this.props.fallback) {
|
|
3020
|
+
return React.createElement(this.props.fallback, {
|
|
3021
|
+
error: this.state.error,
|
|
3022
|
+
reset: () => this.setState({ error: null })
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
3025
|
+
return React.createElement('div', { style: { color: 'red', padding: '20px' } },
|
|
3026
|
+
'Error: ' + this.state.error.message
|
|
3027
|
+
);
|
|
3028
|
+
}
|
|
3029
|
+
return this.props.children;
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
|
|
2402
3033
|
// Wrapper for async Server Components
|
|
2403
3034
|
function AsyncComponent({ component: Component, pathname, search }) {
|
|
2404
3035
|
const [content, setContent] = React.useState(null);
|
|
2405
3036
|
const [error, setError] = React.useState(null);
|
|
3037
|
+
const [isNotFound, setIsNotFound] = React.useState(false);
|
|
2406
3038
|
|
|
2407
3039
|
React.useEffect(() => {
|
|
2408
3040
|
let cancelled = false;
|
|
@@ -2428,20 +3060,42 @@ export class NextDevServer extends DevServer {
|
|
|
2428
3060
|
if (!cancelled) setContent(result);
|
|
2429
3061
|
}
|
|
2430
3062
|
} catch (e) {
|
|
2431
|
-
|
|
2432
|
-
|
|
3063
|
+
if (e && e.message === 'NEXT_NOT_FOUND') {
|
|
3064
|
+
if (!cancelled) setIsNotFound(true);
|
|
3065
|
+
} else {
|
|
3066
|
+
console.error('[AsyncComponent] Error rendering:', e);
|
|
3067
|
+
if (!cancelled) setError(e);
|
|
3068
|
+
}
|
|
2433
3069
|
}
|
|
2434
3070
|
}
|
|
2435
3071
|
render();
|
|
2436
3072
|
return () => { cancelled = true; };
|
|
2437
3073
|
}, [Component, pathname, search]);
|
|
2438
3074
|
|
|
3075
|
+
if (isNotFound && NotFoundComponent) {
|
|
3076
|
+
return React.createElement(NotFoundComponent);
|
|
3077
|
+
}
|
|
3078
|
+
if (isNotFound) {
|
|
3079
|
+
return React.createElement('div', { style: { padding: '20px', textAlign: 'center' } },
|
|
3080
|
+
React.createElement('h2', null, '404'),
|
|
3081
|
+
React.createElement('p', null, 'This page could not be found.')
|
|
3082
|
+
);
|
|
3083
|
+
}
|
|
2439
3084
|
if (error) {
|
|
3085
|
+
if (ErrorComponent) {
|
|
3086
|
+
return React.createElement(ErrorComponent, {
|
|
3087
|
+
error: error,
|
|
3088
|
+
reset: () => setError(null)
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
2440
3091
|
return React.createElement('div', { style: { color: 'red', padding: '20px' } },
|
|
2441
3092
|
'Error: ' + error.message
|
|
2442
3093
|
);
|
|
2443
3094
|
}
|
|
2444
3095
|
if (!content) {
|
|
3096
|
+
if (LoadingComponent) {
|
|
3097
|
+
return React.createElement(LoadingComponent);
|
|
3098
|
+
}
|
|
2445
3099
|
return React.createElement('div', { style: { padding: '20px' } }, 'Loading...');
|
|
2446
3100
|
}
|
|
2447
3101
|
return content;
|
|
@@ -2475,7 +3129,8 @@ export class NextDevServer extends DevServer {
|
|
|
2475
3129
|
if (newPath !== path) {
|
|
2476
3130
|
console.log('[Router] Path changed, loading new page...');
|
|
2477
3131
|
setPath(newPath);
|
|
2478
|
-
const [P, L] = await Promise.all([loadPage(newPath), loadLayouts(newPath)]);
|
|
3132
|
+
const [P, L, routeParams] = await Promise.all([loadPage(newPath), loadLayouts(newPath), extractRouteParams(newPath)]);
|
|
3133
|
+
window.__NEXT_ROUTE_PARAMS__ = routeParams;
|
|
2479
3134
|
console.log('[Router] Page loaded:', !!P, 'Layouts:', L.length);
|
|
2480
3135
|
if (P) setPage(() => P);
|
|
2481
3136
|
setLayouts(L);
|
|
@@ -2493,6 +3148,12 @@ export class NextDevServer extends DevServer {
|
|
|
2493
3148
|
// Use AsyncComponent wrapper to handle async Server Components
|
|
2494
3149
|
// Pass search to force re-render when query params change
|
|
2495
3150
|
let content = React.createElement(AsyncComponent, { component: Page, pathname: path, search: search });
|
|
3151
|
+
|
|
3152
|
+
// Wrap with error boundary if error.tsx exists
|
|
3153
|
+
if (ErrorComponent) {
|
|
3154
|
+
content = React.createElement(ErrorBoundary, { fallback: ErrorComponent }, content);
|
|
3155
|
+
}
|
|
3156
|
+
|
|
2496
3157
|
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
2497
3158
|
content = React.createElement(layouts[i], null, content);
|
|
2498
3159
|
}
|
|
@@ -2683,7 +3344,8 @@ export class NextDevServer extends DevServer {
|
|
|
2683
3344
|
"next/image": "${virtualPrefix}/_next/shims/image.js",
|
|
2684
3345
|
"next/dynamic": "${virtualPrefix}/_next/shims/dynamic.js",
|
|
2685
3346
|
"next/script": "${virtualPrefix}/_next/shims/script.js",
|
|
2686
|
-
"next/font/google": "${virtualPrefix}/_next/shims/font/google.js"
|
|
3347
|
+
"next/font/google": "${virtualPrefix}/_next/shims/font/google.js",
|
|
3348
|
+
"next/font/local": "${virtualPrefix}/_next/shims/font/local.js"
|
|
2687
3349
|
}
|
|
2688
3350
|
}
|
|
2689
3351
|
</script>
|
|
@@ -2910,7 +3572,9 @@ export class NextDevServer extends DevServer {
|
|
|
2910
3572
|
*/
|
|
2911
3573
|
private async transformCode(code: string, filename: string): Promise<string> {
|
|
2912
3574
|
if (!isBrowser) {
|
|
2913
|
-
|
|
3575
|
+
// Even in non-browser mode, strip/transform CSS imports
|
|
3576
|
+
// so CSS module imports get replaced with class name objects
|
|
3577
|
+
return this.stripCssImports(code, filename);
|
|
2914
3578
|
}
|
|
2915
3579
|
|
|
2916
3580
|
await initEsbuild();
|
|
@@ -2922,7 +3586,7 @@ export class NextDevServer extends DevServer {
|
|
|
2922
3586
|
|
|
2923
3587
|
// Remove CSS imports before transformation - they are handled via <link> tags
|
|
2924
3588
|
// CSS imports in ESM would fail with MIME type errors
|
|
2925
|
-
const codeWithoutCssImports = this.stripCssImports(code);
|
|
3589
|
+
const codeWithoutCssImports = this.stripCssImports(code, filename);
|
|
2926
3590
|
|
|
2927
3591
|
// Resolve path aliases (e.g., @/ -> /) before transformation
|
|
2928
3592
|
const codeWithResolvedAliases = this.resolvePathAliases(codeWithoutCssImports, filename);
|
|
@@ -2953,81 +3617,19 @@ export class NextDevServer extends DevServer {
|
|
|
2953
3617
|
return codeWithCdnImports;
|
|
2954
3618
|
}
|
|
2955
3619
|
|
|
2956
|
-
/**
|
|
2957
|
-
* Redirect bare npm package imports to esm.sh CDN
|
|
2958
|
-
* e.g., import { Crisp } from "crisp-sdk-web" -> import { Crisp } from "https://esm.sh/crisp-sdk-web?external=react"
|
|
2959
|
-
*
|
|
2960
|
-
* IMPORTANT: We redirect ALL npm packages to esm.sh URLs (including React)
|
|
2961
|
-
* because import maps don't work reliably for dynamically imported modules.
|
|
2962
|
-
*/
|
|
2963
3620
|
private redirectNpmImports(code: string): string {
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
'react': 'https://esm.sh/react@18.2.0?dev',
|
|
2967
|
-
'react/jsx-runtime': 'https://esm.sh/react@18.2.0&dev/jsx-runtime',
|
|
2968
|
-
'react/jsx-dev-runtime': 'https://esm.sh/react@18.2.0&dev/jsx-dev-runtime',
|
|
2969
|
-
'react-dom': 'https://esm.sh/react-dom@18.2.0?dev',
|
|
2970
|
-
'react-dom/client': 'https://esm.sh/react-dom@18.2.0/client?dev',
|
|
2971
|
-
};
|
|
2972
|
-
|
|
2973
|
-
// Packages that are local or have custom shims (NOT npm packages)
|
|
2974
|
-
const localPackages = new Set([
|
|
2975
|
-
'next/link', 'next/router', 'next/head', 'next/navigation',
|
|
2976
|
-
'next/dynamic', 'next/image', 'next/script', 'next/font/google',
|
|
2977
|
-
'convex/_generated/api'
|
|
2978
|
-
]);
|
|
2979
|
-
|
|
2980
|
-
// Pattern to match import statements with bare package specifiers
|
|
2981
|
-
// Matches: from "package" or from 'package' where package doesn't start with . / or http
|
|
2982
|
-
const importPattern = /(from\s*['"])([^'"./][^'"]*?)(['"])/g;
|
|
2983
|
-
|
|
2984
|
-
return code.replace(importPattern, (match, prefix, packageName, suffix) => {
|
|
2985
|
-
// Skip if already a URL or local virtual path
|
|
2986
|
-
if (packageName.startsWith('http://') ||
|
|
2987
|
-
packageName.startsWith('https://') ||
|
|
2988
|
-
packageName.startsWith('/__virtual__')) {
|
|
2989
|
-
return match;
|
|
2990
|
-
}
|
|
2991
|
-
|
|
2992
|
-
// Check explicit mappings first
|
|
2993
|
-
if (explicitMappings[packageName]) {
|
|
2994
|
-
return `${prefix}${explicitMappings[packageName]}${suffix}`;
|
|
2995
|
-
}
|
|
2996
|
-
|
|
2997
|
-
// Skip local/shimmed packages (they're handled via import map or virtual paths)
|
|
2998
|
-
if (localPackages.has(packageName)) {
|
|
2999
|
-
return match;
|
|
3000
|
-
}
|
|
3001
|
-
|
|
3002
|
-
// Check if it's a subpath import of a local package
|
|
3003
|
-
const basePkg = packageName.includes('/') ? packageName.split('/')[0] : packageName;
|
|
3004
|
-
|
|
3005
|
-
// Handle scoped packages (@org/pkg)
|
|
3006
|
-
const isScoped = basePkg.startsWith('@');
|
|
3007
|
-
const scopedBasePkg = isScoped && packageName.includes('/')
|
|
3008
|
-
? packageName.split('/').slice(0, 2).join('/')
|
|
3009
|
-
: basePkg;
|
|
3010
|
-
|
|
3011
|
-
if (localPackages.has(scopedBasePkg)) {
|
|
3012
|
-
return match;
|
|
3013
|
-
}
|
|
3621
|
+
return _redirectNpmImports(code);
|
|
3622
|
+
}
|
|
3014
3623
|
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
return `${prefix}${esmUrl}${suffix}`;
|
|
3018
|
-
});
|
|
3624
|
+
private stripCssImports(code: string, currentFile?: string): string {
|
|
3625
|
+
return _stripCssImports(code, currentFile, this.getCssModuleContext());
|
|
3019
3626
|
}
|
|
3020
3627
|
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
// Match import statements for CSS files (with or without semicolon)
|
|
3027
|
-
// Handles: import './styles.css'; import "./globals.css" import '../path/file.css'
|
|
3028
|
-
// NOTE: Don't match trailing whitespace (\s*) as it would consume newlines
|
|
3029
|
-
// and break subsequent imports that start on the next line
|
|
3030
|
-
return code.replace(/import\s+['"][^'"]+\.css['"]\s*;?/g, '');
|
|
3628
|
+
private getCssModuleContext(): CssModuleContext {
|
|
3629
|
+
return {
|
|
3630
|
+
readFile: (path: string) => this.vfs.readFileSync(path, 'utf-8'),
|
|
3631
|
+
exists: (path: string) => this.exists(path),
|
|
3632
|
+
};
|
|
3031
3633
|
}
|
|
3032
3634
|
|
|
3033
3635
|
/**
|
|
@@ -3062,94 +3664,11 @@ export class NextDevServer extends DevServer {
|
|
|
3062
3664
|
return result.code;
|
|
3063
3665
|
}
|
|
3064
3666
|
|
|
3065
|
-
|
|
3066
|
-
let transformed = codeWithResolvedAliases;
|
|
3067
|
-
|
|
3068
|
-
// Convert: import X from 'Y' -> const X = require('Y')
|
|
3069
|
-
transformed = transformed.replace(
|
|
3070
|
-
/import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
|
|
3071
|
-
'const $1 = require("$2")'
|
|
3072
|
-
);
|
|
3073
|
-
|
|
3074
|
-
// Convert: import { X } from 'Y' -> const { X } = require('Y')
|
|
3075
|
-
transformed = transformed.replace(
|
|
3076
|
-
/import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g,
|
|
3077
|
-
'const {$1} = require("$2")'
|
|
3078
|
-
);
|
|
3079
|
-
|
|
3080
|
-
// Convert: export default function X -> module.exports = function X
|
|
3081
|
-
transformed = transformed.replace(
|
|
3082
|
-
/export\s+default\s+function\s+(\w+)/g,
|
|
3083
|
-
'module.exports = function $1'
|
|
3084
|
-
);
|
|
3085
|
-
|
|
3086
|
-
// Convert: export default function -> module.exports = function
|
|
3087
|
-
transformed = transformed.replace(
|
|
3088
|
-
/export\s+default\s+function\s*\(/g,
|
|
3089
|
-
'module.exports = function('
|
|
3090
|
-
);
|
|
3091
|
-
|
|
3092
|
-
// Convert: export default X -> module.exports = X
|
|
3093
|
-
transformed = transformed.replace(
|
|
3094
|
-
/export\s+default\s+/g,
|
|
3095
|
-
'module.exports = '
|
|
3096
|
-
);
|
|
3097
|
-
|
|
3098
|
-
return transformed;
|
|
3667
|
+
return transformEsmToCjsSimple(codeWithResolvedAliases);
|
|
3099
3668
|
}
|
|
3100
3669
|
|
|
3101
|
-
/**
|
|
3102
|
-
* Add React Refresh registration to transformed code
|
|
3103
|
-
*/
|
|
3104
3670
|
private addReactRefresh(code: string, filename: string): string {
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
const funcDeclRegex = /(?:^|\n)(?:export\s+)?function\s+([A-Z][a-zA-Z0-9]*)\s*\(/g;
|
|
3108
|
-
let match;
|
|
3109
|
-
while ((match = funcDeclRegex.exec(code)) !== null) {
|
|
3110
|
-
if (!components.includes(match[1])) {
|
|
3111
|
-
components.push(match[1]);
|
|
3112
|
-
}
|
|
3113
|
-
}
|
|
3114
|
-
|
|
3115
|
-
const arrowRegex = /(?:^|\n)(?:export\s+)?(?:const|let|var)\s+([A-Z][a-zA-Z0-9]*)\s*=/g;
|
|
3116
|
-
while ((match = arrowRegex.exec(code)) !== null) {
|
|
3117
|
-
if (!components.includes(match[1])) {
|
|
3118
|
-
components.push(match[1]);
|
|
3119
|
-
}
|
|
3120
|
-
}
|
|
3121
|
-
|
|
3122
|
-
if (components.length === 0) {
|
|
3123
|
-
return `// HMR Setup
|
|
3124
|
-
import.meta.hot = window.__vite_hot_context__("${filename}");
|
|
3125
|
-
|
|
3126
|
-
${code}
|
|
3127
|
-
|
|
3128
|
-
if (import.meta.hot) {
|
|
3129
|
-
import.meta.hot.accept();
|
|
3130
|
-
}
|
|
3131
|
-
`;
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
const registrations = components
|
|
3135
|
-
.map(name => ` $RefreshReg$(${name}, "${filename} ${name}");`)
|
|
3136
|
-
.join('\n');
|
|
3137
|
-
|
|
3138
|
-
return `// HMR Setup
|
|
3139
|
-
import.meta.hot = window.__vite_hot_context__("${filename}");
|
|
3140
|
-
|
|
3141
|
-
${code}
|
|
3142
|
-
|
|
3143
|
-
// React Refresh Registration
|
|
3144
|
-
if (import.meta.hot) {
|
|
3145
|
-
${registrations}
|
|
3146
|
-
import.meta.hot.accept(() => {
|
|
3147
|
-
if (window.$RefreshRuntime$) {
|
|
3148
|
-
window.$RefreshRuntime$.performReactRefresh();
|
|
3149
|
-
}
|
|
3150
|
-
});
|
|
3151
|
-
}
|
|
3152
|
-
`;
|
|
3671
|
+
return _addReactRefresh(code, filename);
|
|
3153
3672
|
}
|
|
3154
3673
|
|
|
3155
3674
|
/**
|