almostnode 0.2.6 → 0.2.8
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 +1 -1
- package/dist/__sw__.js +80 -84
- package/dist/assets/{runtime-worker-B8_LZkBX.js → runtime-worker-D8VYeuKv.js} +1448 -1121
- package/dist/assets/runtime-worker-D8VYeuKv.js.map +1 -0
- package/dist/frameworks/code-transforms.d.ts +53 -0
- package/dist/frameworks/code-transforms.d.ts.map +1 -0
- package/dist/frameworks/next-config-parser.d.ts +16 -0
- package/dist/frameworks/next-config-parser.d.ts.map +1 -0
- package/dist/frameworks/next-dev-server.d.ts +29 -18
- package/dist/frameworks/next-dev-server.d.ts.map +1 -1
- package/dist/frameworks/next-html-generator.d.ts +35 -0
- package/dist/frameworks/next-html-generator.d.ts.map +1 -0
- package/dist/frameworks/next-shims.d.ts +79 -0
- package/dist/frameworks/next-shims.d.ts.map +1 -0
- 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 +30392 -9523
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +27296 -8797
- package/dist/index.mjs.map +1 -1
- package/dist/runtime.d.ts +20 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/server-bridge.d.ts +2 -0
- package/dist/server-bridge.d.ts.map +1 -1
- package/dist/shims/crypto.d.ts +2 -0
- package/dist/shims/crypto.d.ts.map +1 -1
- package/dist/shims/esbuild.d.ts.map +1 -1
- package/dist/shims/fs.d.ts.map +1 -1
- package/dist/shims/http.d.ts +29 -0
- package/dist/shims/http.d.ts.map +1 -1
- package/dist/shims/path.d.ts.map +1 -1
- package/dist/shims/stream.d.ts.map +1 -1
- package/dist/shims/vfs-adapter.d.ts.map +1 -1
- package/dist/shims/ws.d.ts +2 -0
- package/dist/shims/ws.d.ts.map +1 -1
- package/dist/utils/binary-encoding.d.ts +13 -0
- package/dist/utils/binary-encoding.d.ts.map +1 -0
- package/dist/virtual-fs.d.ts.map +1 -1
- package/package.json +8 -4
- package/src/convex-app-demo-entry.ts +231 -35
- package/src/frameworks/code-transforms.ts +581 -0
- package/src/frameworks/next-config-parser.ts +140 -0
- package/src/frameworks/next-dev-server.ts +561 -1641
- package/src/frameworks/next-html-generator.ts +597 -0
- package/src/frameworks/next-shims.ts +1050 -0
- package/src/frameworks/tailwind-config-loader.ts +1 -1
- package/src/frameworks/vite-dev-server.ts +2 -61
- package/src/index.ts +2 -0
- package/src/runtime.ts +94 -15
- package/src/server-bridge.ts +61 -28
- package/src/shims/crypto.ts +13 -0
- package/src/shims/esbuild.ts +4 -1
- package/src/shims/fs.ts +9 -11
- package/src/shims/http.ts +309 -3
- package/src/shims/path.ts +6 -13
- package/src/shims/stream.ts +12 -26
- package/src/shims/vfs-adapter.ts +5 -2
- package/src/shims/ws.ts +92 -2
- package/src/utils/binary-encoding.ts +43 -0
- package/src/virtual-fs.ts +7 -15
- package/dist/assets/runtime-worker-B8_LZkBX.js.map +0 -1
|
@@ -8,6 +8,31 @@ 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 { parseNextConfigValue } from './next-config-parser';
|
|
12
|
+
import {
|
|
13
|
+
redirectNpmImports as _redirectNpmImports,
|
|
14
|
+
stripCssImports as _stripCssImports,
|
|
15
|
+
addReactRefresh as _addReactRefresh,
|
|
16
|
+
transformEsmToCjsSimple,
|
|
17
|
+
type CssModuleContext,
|
|
18
|
+
} from './code-transforms';
|
|
19
|
+
import {
|
|
20
|
+
NEXT_LINK_SHIM,
|
|
21
|
+
NEXT_ROUTER_SHIM,
|
|
22
|
+
NEXT_NAVIGATION_SHIM,
|
|
23
|
+
NEXT_HEAD_SHIM,
|
|
24
|
+
NEXT_IMAGE_SHIM,
|
|
25
|
+
NEXT_DYNAMIC_SHIM,
|
|
26
|
+
NEXT_SCRIPT_SHIM,
|
|
27
|
+
NEXT_FONT_GOOGLE_SHIM,
|
|
28
|
+
NEXT_FONT_LOCAL_SHIM,
|
|
29
|
+
} from './next-shims';
|
|
30
|
+
import {
|
|
31
|
+
type AppRoute,
|
|
32
|
+
generateAppRouterHtml as _generateAppRouterHtml,
|
|
33
|
+
generatePageHtml as _generatePageHtml,
|
|
34
|
+
serve404Page as _serve404Page,
|
|
35
|
+
} from './next-html-generator';
|
|
11
36
|
|
|
12
37
|
// Check if we're in a real browser environment (not jsdom or Node.js)
|
|
13
38
|
const isBrowser = typeof window !== 'undefined' &&
|
|
@@ -80,922 +105,10 @@ export interface NextDevServerOptions extends DevServerOptions {
|
|
|
80
105
|
env?: Record<string, string>;
|
|
81
106
|
/** Asset prefix for static files (e.g., '/marketing'). Auto-detected from next.config if not specified. */
|
|
82
107
|
assetPrefix?: string;
|
|
108
|
+
/** Base path for the app (e.g., '/docs'). Auto-detected from next.config if not specified. */
|
|
109
|
+
basePath?: string;
|
|
83
110
|
}
|
|
84
111
|
|
|
85
|
-
/**
|
|
86
|
-
* Tailwind CSS CDN script for runtime JIT compilation
|
|
87
|
-
*/
|
|
88
|
-
const TAILWIND_CDN_SCRIPT = `<script src="https://cdn.tailwindcss.com"></script>`;
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* CORS Proxy script - provides proxyFetch function in the iframe
|
|
92
|
-
* Reads proxy URL from localStorage (set by parent window)
|
|
93
|
-
*/
|
|
94
|
-
const CORS_PROXY_SCRIPT = `
|
|
95
|
-
<script>
|
|
96
|
-
// CORS Proxy support for external API calls
|
|
97
|
-
window.__getCorsProxy = function() {
|
|
98
|
-
return localStorage.getItem('__corsProxyUrl') || null;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
window.__setCorsProxy = function(url) {
|
|
102
|
-
if (url) {
|
|
103
|
-
localStorage.setItem('__corsProxyUrl', url);
|
|
104
|
-
} else {
|
|
105
|
-
localStorage.removeItem('__corsProxyUrl');
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
window.__proxyFetch = async function(url, options) {
|
|
110
|
-
const proxyUrl = window.__getCorsProxy();
|
|
111
|
-
if (proxyUrl) {
|
|
112
|
-
const proxiedUrl = proxyUrl + encodeURIComponent(url);
|
|
113
|
-
return fetch(proxiedUrl, options);
|
|
114
|
-
}
|
|
115
|
-
return fetch(url, options);
|
|
116
|
-
};
|
|
117
|
-
</script>
|
|
118
|
-
`;
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* React Refresh preamble - MUST run before React is loaded
|
|
122
|
-
*/
|
|
123
|
-
const REACT_REFRESH_PREAMBLE = `
|
|
124
|
-
<script type="module">
|
|
125
|
-
// Block until React Refresh is loaded and initialized
|
|
126
|
-
const RefreshRuntime = await import('https://esm.sh/react-refresh@0.14.0/runtime').then(m => m.default || m);
|
|
127
|
-
|
|
128
|
-
RefreshRuntime.injectIntoGlobalHook(window);
|
|
129
|
-
window.$RefreshRuntime$ = RefreshRuntime;
|
|
130
|
-
window.$RefreshRegCount$ = 0;
|
|
131
|
-
|
|
132
|
-
window.$RefreshReg$ = (type, id) => {
|
|
133
|
-
window.$RefreshRegCount$++;
|
|
134
|
-
RefreshRuntime.register(type, id);
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
window.$RefreshSig$ = () => (type) => type;
|
|
138
|
-
|
|
139
|
-
console.log('[HMR] React Refresh initialized');
|
|
140
|
-
</script>
|
|
141
|
-
`;
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* HMR client script for Next.js
|
|
145
|
-
*/
|
|
146
|
-
const HMR_CLIENT_SCRIPT = `
|
|
147
|
-
<script type="module">
|
|
148
|
-
(function() {
|
|
149
|
-
const hotModules = new Map();
|
|
150
|
-
const pendingUpdates = new Map();
|
|
151
|
-
|
|
152
|
-
window.__vite_hot_context__ = function createHotContext(ownerPath) {
|
|
153
|
-
if (hotModules.has(ownerPath)) {
|
|
154
|
-
return hotModules.get(ownerPath);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const hot = {
|
|
158
|
-
data: {},
|
|
159
|
-
accept(callback) {
|
|
160
|
-
hot._acceptCallback = callback;
|
|
161
|
-
},
|
|
162
|
-
dispose(callback) {
|
|
163
|
-
hot._disposeCallback = callback;
|
|
164
|
-
},
|
|
165
|
-
invalidate() {
|
|
166
|
-
location.reload();
|
|
167
|
-
},
|
|
168
|
-
prune(callback) {
|
|
169
|
-
hot._pruneCallback = callback;
|
|
170
|
-
},
|
|
171
|
-
on(event, cb) {},
|
|
172
|
-
off(event, cb) {},
|
|
173
|
-
send(event, data) {},
|
|
174
|
-
_acceptCallback: null,
|
|
175
|
-
_disposeCallback: null,
|
|
176
|
-
_pruneCallback: null,
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
hotModules.set(ownerPath, hot);
|
|
180
|
-
return hot;
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
// Listen for HMR updates via postMessage (works with sandboxed iframes)
|
|
184
|
-
window.addEventListener('message', async (event) => {
|
|
185
|
-
// Filter for HMR messages only
|
|
186
|
-
if (!event.data || event.data.channel !== 'next-hmr') return;
|
|
187
|
-
const { type, path, timestamp } = event.data;
|
|
188
|
-
|
|
189
|
-
if (type === 'update') {
|
|
190
|
-
console.log('[HMR] Update:', path);
|
|
191
|
-
|
|
192
|
-
if (path.endsWith('.css')) {
|
|
193
|
-
const links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
194
|
-
links.forEach(link => {
|
|
195
|
-
const href = link.getAttribute('href');
|
|
196
|
-
if (href && href.includes(path.replace(/^\\//, ''))) {
|
|
197
|
-
link.href = href.split('?')[0] + '?t=' + timestamp;
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const styles = document.querySelectorAll('style[data-next-dev-id]');
|
|
202
|
-
styles.forEach(style => {
|
|
203
|
-
const id = style.getAttribute('data-next-dev-id');
|
|
204
|
-
if (id && id.includes(path.replace(/^\\//, ''))) {
|
|
205
|
-
import(path + '?t=' + timestamp).catch(() => {});
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
} else if (path.match(/\\.(jsx?|tsx?)$/)) {
|
|
209
|
-
await handleJSUpdate(path, timestamp);
|
|
210
|
-
}
|
|
211
|
-
} else if (type === 'full-reload') {
|
|
212
|
-
console.log('[HMR] Full reload');
|
|
213
|
-
location.reload();
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
async function handleJSUpdate(path, timestamp) {
|
|
218
|
-
const normalizedPath = path.startsWith('/') ? path : '/' + path;
|
|
219
|
-
const hot = hotModules.get(normalizedPath);
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
if (hot && hot._disposeCallback) {
|
|
223
|
-
hot._disposeCallback(hot.data);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (window.$RefreshRuntime$) {
|
|
227
|
-
pendingUpdates.set(normalizedPath, timestamp);
|
|
228
|
-
|
|
229
|
-
if (pendingUpdates.size === 1) {
|
|
230
|
-
setTimeout(async () => {
|
|
231
|
-
try {
|
|
232
|
-
for (const [modulePath, ts] of pendingUpdates) {
|
|
233
|
-
const moduleUrl = '.' + modulePath + '?t=' + ts;
|
|
234
|
-
await import(moduleUrl);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
window.$RefreshRuntime$.performReactRefresh();
|
|
238
|
-
console.log('[HMR] Updated', pendingUpdates.size, 'module(s)');
|
|
239
|
-
|
|
240
|
-
pendingUpdates.clear();
|
|
241
|
-
} catch (error) {
|
|
242
|
-
console.error('[HMR] Failed to apply update:', error);
|
|
243
|
-
pendingUpdates.clear();
|
|
244
|
-
location.reload();
|
|
245
|
-
}
|
|
246
|
-
}, 30);
|
|
247
|
-
}
|
|
248
|
-
} else {
|
|
249
|
-
console.log('[HMR] React Refresh not available, reloading page');
|
|
250
|
-
location.reload();
|
|
251
|
-
}
|
|
252
|
-
} catch (error) {
|
|
253
|
-
console.error('[HMR] Update failed:', error);
|
|
254
|
-
location.reload();
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
console.log('[HMR] Next.js client ready');
|
|
259
|
-
})();
|
|
260
|
-
</script>
|
|
261
|
-
`;
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Next.js Link shim code
|
|
265
|
-
*/
|
|
266
|
-
const NEXT_LINK_SHIM = `
|
|
267
|
-
import React from 'react';
|
|
268
|
-
|
|
269
|
-
const getVirtualBasePath = () => {
|
|
270
|
-
const match = window.location.pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
|
|
271
|
-
if (!match) return '';
|
|
272
|
-
return match[0].endsWith('/') ? match[0] : match[0] + '/';
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
const applyVirtualBase = (url) => {
|
|
276
|
-
if (typeof url !== 'string') return url;
|
|
277
|
-
if (!url || url.startsWith('#') || url.startsWith('?')) return url;
|
|
278
|
-
if (/^(https?:)?\\/\\//.test(url)) return url;
|
|
279
|
-
|
|
280
|
-
const base = getVirtualBasePath();
|
|
281
|
-
if (!base) return url;
|
|
282
|
-
if (url.startsWith(base)) return url;
|
|
283
|
-
if (url.startsWith('/')) return base + url.slice(1);
|
|
284
|
-
return base + url;
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
export default function Link({ href, children, ...props }) {
|
|
288
|
-
const handleClick = (e) => {
|
|
289
|
-
console.log('[Link] Click handler called, href:', href);
|
|
290
|
-
|
|
291
|
-
if (props.onClick) {
|
|
292
|
-
props.onClick(e);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Allow cmd/ctrl click to open in new tab
|
|
296
|
-
if (e.metaKey || e.ctrlKey) {
|
|
297
|
-
console.log('[Link] Meta/Ctrl key pressed, allowing default behavior');
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (typeof href !== 'string' || !href || href.startsWith('#') || href.startsWith('?')) {
|
|
302
|
-
console.log('[Link] Skipping navigation for href:', href);
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (/^(https?:)?\\/\\//.test(href)) {
|
|
307
|
-
console.log('[Link] External URL, allowing default behavior:', href);
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
e.preventDefault();
|
|
312
|
-
const resolvedHref = applyVirtualBase(href);
|
|
313
|
-
console.log('[Link] Navigating to:', resolvedHref);
|
|
314
|
-
window.history.pushState({}, '', resolvedHref);
|
|
315
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
return React.createElement('a', { href, onClick: handleClick, ...props }, children);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export { Link };
|
|
322
|
-
`;
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Next.js Router shim code
|
|
326
|
-
*/
|
|
327
|
-
const NEXT_ROUTER_SHIM = `
|
|
328
|
-
import React, { useState, useEffect, createContext, useContext } from 'react';
|
|
329
|
-
|
|
330
|
-
const RouterContext = createContext(null);
|
|
331
|
-
|
|
332
|
-
const getVirtualBasePath = () => {
|
|
333
|
-
const match = window.location.pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
|
|
334
|
-
if (!match) return '';
|
|
335
|
-
return match[0].endsWith('/') ? match[0] : match[0] + '/';
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
const applyVirtualBase = (url) => {
|
|
339
|
-
if (typeof url !== 'string') return url;
|
|
340
|
-
if (!url || url.startsWith('#') || url.startsWith('?')) return url;
|
|
341
|
-
if (/^(https?:)?\\/\\//.test(url)) return url;
|
|
342
|
-
|
|
343
|
-
const base = getVirtualBasePath();
|
|
344
|
-
if (!base) return url;
|
|
345
|
-
if (url.startsWith(base)) return url;
|
|
346
|
-
if (url.startsWith('/')) return base + url.slice(1);
|
|
347
|
-
return base + url;
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
const stripVirtualBase = (pathname) => {
|
|
351
|
-
const match = pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
|
|
352
|
-
if (!match) return pathname;
|
|
353
|
-
return '/' + pathname.slice(match[0].length);
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
export function useRouter() {
|
|
357
|
-
const [pathname, setPathname] = useState(
|
|
358
|
-
typeof window !== 'undefined' ? stripVirtualBase(window.location.pathname) : '/'
|
|
359
|
-
);
|
|
360
|
-
const [query, setQuery] = useState({});
|
|
361
|
-
|
|
362
|
-
useEffect(() => {
|
|
363
|
-
const updateRoute = () => {
|
|
364
|
-
setPathname(stripVirtualBase(window.location.pathname));
|
|
365
|
-
setQuery(Object.fromEntries(new URLSearchParams(window.location.search)));
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
window.addEventListener('popstate', updateRoute);
|
|
369
|
-
updateRoute();
|
|
370
|
-
|
|
371
|
-
return () => window.removeEventListener('popstate', updateRoute);
|
|
372
|
-
}, []);
|
|
373
|
-
|
|
374
|
-
return {
|
|
375
|
-
pathname,
|
|
376
|
-
query,
|
|
377
|
-
asPath: pathname + window.location.search,
|
|
378
|
-
push: (url, as, options) => {
|
|
379
|
-
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
380
|
-
window.location.href = url;
|
|
381
|
-
return Promise.resolve(true);
|
|
382
|
-
}
|
|
383
|
-
const resolvedUrl = applyVirtualBase(url);
|
|
384
|
-
window.history.pushState({}, '', resolvedUrl);
|
|
385
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
386
|
-
return Promise.resolve(true);
|
|
387
|
-
},
|
|
388
|
-
replace: (url, as, options) => {
|
|
389
|
-
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
390
|
-
window.location.href = url;
|
|
391
|
-
return Promise.resolve(true);
|
|
392
|
-
}
|
|
393
|
-
const resolvedUrl = applyVirtualBase(url);
|
|
394
|
-
window.history.replaceState({}, '', resolvedUrl);
|
|
395
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
396
|
-
return Promise.resolve(true);
|
|
397
|
-
},
|
|
398
|
-
prefetch: () => Promise.resolve(),
|
|
399
|
-
back: () => window.history.back(),
|
|
400
|
-
forward: () => window.history.forward(),
|
|
401
|
-
reload: () => window.location.reload(),
|
|
402
|
-
events: {
|
|
403
|
-
on: () => {},
|
|
404
|
-
off: () => {},
|
|
405
|
-
emit: () => {},
|
|
406
|
-
},
|
|
407
|
-
isFallback: false,
|
|
408
|
-
isReady: true,
|
|
409
|
-
isPreview: false,
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
export const Router = {
|
|
414
|
-
events: {
|
|
415
|
-
on: () => {},
|
|
416
|
-
off: () => {},
|
|
417
|
-
emit: () => {},
|
|
418
|
-
},
|
|
419
|
-
push: (url) => {
|
|
420
|
-
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
421
|
-
window.location.href = url;
|
|
422
|
-
return Promise.resolve(true);
|
|
423
|
-
}
|
|
424
|
-
const resolvedUrl = applyVirtualBase(url);
|
|
425
|
-
window.history.pushState({}, '', resolvedUrl);
|
|
426
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
427
|
-
return Promise.resolve(true);
|
|
428
|
-
},
|
|
429
|
-
replace: (url) => {
|
|
430
|
-
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
431
|
-
window.location.href = url;
|
|
432
|
-
return Promise.resolve(true);
|
|
433
|
-
}
|
|
434
|
-
const resolvedUrl = applyVirtualBase(url);
|
|
435
|
-
window.history.replaceState({}, '', resolvedUrl);
|
|
436
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
437
|
-
return Promise.resolve(true);
|
|
438
|
-
},
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
export default { useRouter, Router };
|
|
442
|
-
`;
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Next.js Navigation shim code (App Router)
|
|
446
|
-
*
|
|
447
|
-
* This shim provides App Router-specific navigation hooks from 'next/navigation'.
|
|
448
|
-
* These are DIFFERENT from the Pages Router hooks in 'next/router':
|
|
449
|
-
*
|
|
450
|
-
* Pages Router (next/router):
|
|
451
|
-
* - useRouter() returns { pathname, query, push, replace, events, ... }
|
|
452
|
-
* - Has router.events for route change subscriptions
|
|
453
|
-
* - query object contains URL params
|
|
454
|
-
*
|
|
455
|
-
* App Router (next/navigation):
|
|
456
|
-
* - useRouter() returns { push, replace, back, forward, refresh, prefetch }
|
|
457
|
-
* - usePathname() for current path
|
|
458
|
-
* - useSearchParams() for URL search params
|
|
459
|
-
* - useParams() for dynamic route segments
|
|
460
|
-
* - No events - use useEffect with pathname/searchParams instead
|
|
461
|
-
*
|
|
462
|
-
* @see https://nextjs.org/docs/app/api-reference/functions/use-router
|
|
463
|
-
*/
|
|
464
|
-
const NEXT_NAVIGATION_SHIM = `
|
|
465
|
-
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
466
|
-
|
|
467
|
-
const getVirtualBasePath = () => {
|
|
468
|
-
const match = window.location.pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
|
|
469
|
-
if (!match) return '';
|
|
470
|
-
return match[0].endsWith('/') ? match[0] : match[0] + '/';
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
const applyVirtualBase = (url) => {
|
|
474
|
-
if (typeof url !== 'string') return url;
|
|
475
|
-
if (!url || url.startsWith('#') || url.startsWith('?')) return url;
|
|
476
|
-
if (/^(https?:)?\\/\\//.test(url)) return url;
|
|
477
|
-
|
|
478
|
-
const base = getVirtualBasePath();
|
|
479
|
-
if (!base) return url;
|
|
480
|
-
if (url.startsWith(base)) return url;
|
|
481
|
-
if (url.startsWith('/')) return base + url.slice(1);
|
|
482
|
-
return base + url;
|
|
483
|
-
};
|
|
484
|
-
|
|
485
|
-
const stripVirtualBase = (pathname) => {
|
|
486
|
-
const match = pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
|
|
487
|
-
if (!match) return pathname;
|
|
488
|
-
return '/' + pathname.slice(match[0].length);
|
|
489
|
-
};
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* App Router's useRouter hook
|
|
493
|
-
* Returns navigation methods only (no pathname, no query)
|
|
494
|
-
* Use usePathname() and useSearchParams() for URL info
|
|
495
|
-
*/
|
|
496
|
-
export function useRouter() {
|
|
497
|
-
const push = useCallback((url, options) => {
|
|
498
|
-
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
499
|
-
window.location.href = url;
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
const resolvedUrl = applyVirtualBase(url);
|
|
503
|
-
window.history.pushState({}, '', resolvedUrl);
|
|
504
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
505
|
-
}, []);
|
|
506
|
-
|
|
507
|
-
const replace = useCallback((url, options) => {
|
|
508
|
-
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
509
|
-
window.location.href = url;
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
const resolvedUrl = applyVirtualBase(url);
|
|
513
|
-
window.history.replaceState({}, '', resolvedUrl);
|
|
514
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
515
|
-
}, []);
|
|
516
|
-
|
|
517
|
-
const back = useCallback(() => window.history.back(), []);
|
|
518
|
-
const forward = useCallback(() => window.history.forward(), []);
|
|
519
|
-
const refresh = useCallback(() => window.location.reload(), []);
|
|
520
|
-
const prefetch = useCallback(() => Promise.resolve(), []);
|
|
521
|
-
|
|
522
|
-
return useMemo(() => ({
|
|
523
|
-
push,
|
|
524
|
-
replace,
|
|
525
|
-
back,
|
|
526
|
-
forward,
|
|
527
|
-
refresh,
|
|
528
|
-
prefetch,
|
|
529
|
-
}), [push, replace, back, forward, refresh, prefetch]);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/**
|
|
533
|
-
* usePathname - Returns the current URL pathname
|
|
534
|
-
* Reactively updates when navigation occurs
|
|
535
|
-
* @example const pathname = usePathname(); // '/dashboard/settings'
|
|
536
|
-
*/
|
|
537
|
-
export function usePathname() {
|
|
538
|
-
const [pathname, setPathname] = useState(
|
|
539
|
-
typeof window !== 'undefined' ? stripVirtualBase(window.location.pathname) : '/'
|
|
540
|
-
);
|
|
541
|
-
|
|
542
|
-
useEffect(() => {
|
|
543
|
-
const handler = () => setPathname(stripVirtualBase(window.location.pathname));
|
|
544
|
-
window.addEventListener('popstate', handler);
|
|
545
|
-
return () => window.removeEventListener('popstate', handler);
|
|
546
|
-
}, []);
|
|
547
|
-
|
|
548
|
-
return pathname;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* useSearchParams - Returns the current URL search parameters
|
|
553
|
-
* @example const searchParams = useSearchParams();
|
|
554
|
-
* const query = searchParams.get('q'); // '?q=hello' -> 'hello'
|
|
555
|
-
*/
|
|
556
|
-
export function useSearchParams() {
|
|
557
|
-
const [searchParams, setSearchParams] = useState(() => {
|
|
558
|
-
if (typeof window === 'undefined') return new URLSearchParams();
|
|
559
|
-
return new URLSearchParams(window.location.search);
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
useEffect(() => {
|
|
563
|
-
const handler = () => {
|
|
564
|
-
setSearchParams(new URLSearchParams(window.location.search));
|
|
565
|
-
};
|
|
566
|
-
window.addEventListener('popstate', handler);
|
|
567
|
-
return () => window.removeEventListener('popstate', handler);
|
|
568
|
-
}, []);
|
|
569
|
-
|
|
570
|
-
return searchParams;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* useParams - Returns dynamic route parameters
|
|
575
|
-
* For route /users/[id]/page.jsx with URL /users/123:
|
|
576
|
-
* @example const { id } = useParams(); // { id: '123' }
|
|
577
|
-
*
|
|
578
|
-
* NOTE: This simplified implementation returns empty object.
|
|
579
|
-
* Full implementation would need route pattern matching.
|
|
580
|
-
*/
|
|
581
|
-
export function useParams() {
|
|
582
|
-
// In a real implementation, this would parse the current route
|
|
583
|
-
// against the route pattern to extract params
|
|
584
|
-
// For now, return empty object - works for basic cases
|
|
585
|
-
return {};
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* useSelectedLayoutSegment - Returns the active child segment one level below
|
|
590
|
-
* Useful for styling active nav items in layouts
|
|
591
|
-
* @example For /dashboard/settings, returns 'settings' in dashboard layout
|
|
592
|
-
*/
|
|
593
|
-
export function useSelectedLayoutSegment() {
|
|
594
|
-
const pathname = usePathname();
|
|
595
|
-
const segments = pathname.split('/').filter(Boolean);
|
|
596
|
-
return segments[0] || null;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* useSelectedLayoutSegments - Returns all active child segments
|
|
601
|
-
* @example For /dashboard/settings/profile, returns ['dashboard', 'settings', 'profile']
|
|
602
|
-
*/
|
|
603
|
-
export function useSelectedLayoutSegments() {
|
|
604
|
-
const pathname = usePathname();
|
|
605
|
-
return pathname.split('/').filter(Boolean);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
* redirect - Programmatic redirect (typically used in Server Components)
|
|
610
|
-
* In this browser implementation, performs immediate navigation
|
|
611
|
-
*/
|
|
612
|
-
export function redirect(url) {
|
|
613
|
-
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
614
|
-
window.location.href = url;
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
window.location.href = applyVirtualBase(url);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* notFound - Trigger the not-found UI
|
|
622
|
-
* In this browser implementation, throws an error
|
|
623
|
-
*/
|
|
624
|
-
export function notFound() {
|
|
625
|
-
throw new Error('NEXT_NOT_FOUND');
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Re-export Link for convenience (can import from next/navigation or next/link)
|
|
629
|
-
export { default as Link } from 'next/link';
|
|
630
|
-
`;
|
|
631
|
-
|
|
632
|
-
/**
|
|
633
|
-
* Next.js Head shim code
|
|
634
|
-
*/
|
|
635
|
-
const NEXT_HEAD_SHIM = `
|
|
636
|
-
import React, { useEffect } from 'react';
|
|
637
|
-
|
|
638
|
-
export default function Head({ children }) {
|
|
639
|
-
useEffect(() => {
|
|
640
|
-
// Process children and update document.head
|
|
641
|
-
React.Children.forEach(children, (child) => {
|
|
642
|
-
if (!React.isValidElement(child)) return;
|
|
643
|
-
|
|
644
|
-
const { type, props } = child;
|
|
645
|
-
|
|
646
|
-
if (type === 'title' && props.children) {
|
|
647
|
-
document.title = Array.isArray(props.children)
|
|
648
|
-
? props.children.join('')
|
|
649
|
-
: props.children;
|
|
650
|
-
} else if (type === 'meta') {
|
|
651
|
-
const existingMeta = props.name
|
|
652
|
-
? document.querySelector(\`meta[name="\${props.name}"]\`)
|
|
653
|
-
: props.property
|
|
654
|
-
? document.querySelector(\`meta[property="\${props.property}"]\`)
|
|
655
|
-
: null;
|
|
656
|
-
|
|
657
|
-
if (existingMeta) {
|
|
658
|
-
Object.keys(props).forEach(key => {
|
|
659
|
-
existingMeta.setAttribute(key, props[key]);
|
|
660
|
-
});
|
|
661
|
-
} else {
|
|
662
|
-
const meta = document.createElement('meta');
|
|
663
|
-
Object.keys(props).forEach(key => {
|
|
664
|
-
meta.setAttribute(key, props[key]);
|
|
665
|
-
});
|
|
666
|
-
document.head.appendChild(meta);
|
|
667
|
-
}
|
|
668
|
-
} else if (type === 'link') {
|
|
669
|
-
const link = document.createElement('link');
|
|
670
|
-
Object.keys(props).forEach(key => {
|
|
671
|
-
link.setAttribute(key, props[key]);
|
|
672
|
-
});
|
|
673
|
-
document.head.appendChild(link);
|
|
674
|
-
}
|
|
675
|
-
});
|
|
676
|
-
}, [children]);
|
|
677
|
-
|
|
678
|
-
return null;
|
|
679
|
-
}
|
|
680
|
-
`;
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* Next.js Image shim code
|
|
684
|
-
* Provides a simple img-based implementation of next/image
|
|
685
|
-
*/
|
|
686
|
-
const NEXT_IMAGE_SHIM = `
|
|
687
|
-
import React from 'react';
|
|
688
|
-
|
|
689
|
-
function Image({
|
|
690
|
-
src,
|
|
691
|
-
alt = '',
|
|
692
|
-
width,
|
|
693
|
-
height,
|
|
694
|
-
fill,
|
|
695
|
-
loader,
|
|
696
|
-
quality = 75,
|
|
697
|
-
priority,
|
|
698
|
-
loading,
|
|
699
|
-
placeholder,
|
|
700
|
-
blurDataURL,
|
|
701
|
-
unoptimized,
|
|
702
|
-
onLoad,
|
|
703
|
-
onError,
|
|
704
|
-
style,
|
|
705
|
-
className,
|
|
706
|
-
sizes,
|
|
707
|
-
...rest
|
|
708
|
-
}) {
|
|
709
|
-
// Handle src - could be string or StaticImageData object
|
|
710
|
-
const imageSrc = typeof src === 'object' ? src.src : src;
|
|
711
|
-
|
|
712
|
-
// Build style object
|
|
713
|
-
const imgStyle = { ...style };
|
|
714
|
-
if (fill) {
|
|
715
|
-
imgStyle.position = 'absolute';
|
|
716
|
-
imgStyle.width = '100%';
|
|
717
|
-
imgStyle.height = '100%';
|
|
718
|
-
imgStyle.objectFit = imgStyle.objectFit || 'cover';
|
|
719
|
-
imgStyle.inset = '0';
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
return React.createElement('img', {
|
|
723
|
-
src: imageSrc,
|
|
724
|
-
alt,
|
|
725
|
-
width: fill ? undefined : width,
|
|
726
|
-
height: fill ? undefined : height,
|
|
727
|
-
loading: priority ? 'eager' : (loading || 'lazy'),
|
|
728
|
-
decoding: 'async',
|
|
729
|
-
style: imgStyle,
|
|
730
|
-
className,
|
|
731
|
-
onLoad,
|
|
732
|
-
onError,
|
|
733
|
-
...rest
|
|
734
|
-
});
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
export default Image;
|
|
738
|
-
export { Image };
|
|
739
|
-
`;
|
|
740
|
-
|
|
741
|
-
/**
|
|
742
|
-
* next/dynamic shim - Dynamic imports with loading states
|
|
743
|
-
*/
|
|
744
|
-
const NEXT_DYNAMIC_SHIM = `
|
|
745
|
-
import React from 'react';
|
|
746
|
-
|
|
747
|
-
function dynamic(importFn, options = {}) {
|
|
748
|
-
const {
|
|
749
|
-
loading: LoadingComponent,
|
|
750
|
-
ssr = true,
|
|
751
|
-
} = options;
|
|
752
|
-
|
|
753
|
-
// Create a lazy component
|
|
754
|
-
const LazyComponent = React.lazy(importFn);
|
|
755
|
-
|
|
756
|
-
// Wrapper component that handles loading state
|
|
757
|
-
function DynamicComponent(props) {
|
|
758
|
-
const fallback = LoadingComponent
|
|
759
|
-
? React.createElement(LoadingComponent, { isLoading: true })
|
|
760
|
-
: null;
|
|
761
|
-
|
|
762
|
-
return React.createElement(
|
|
763
|
-
React.Suspense,
|
|
764
|
-
{ fallback },
|
|
765
|
-
React.createElement(LazyComponent, props)
|
|
766
|
-
);
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
return DynamicComponent;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
export default dynamic;
|
|
773
|
-
export { dynamic };
|
|
774
|
-
`;
|
|
775
|
-
|
|
776
|
-
/**
|
|
777
|
-
* next/script shim - Loads external scripts
|
|
778
|
-
*/
|
|
779
|
-
const NEXT_SCRIPT_SHIM = `
|
|
780
|
-
import React from 'react';
|
|
781
|
-
|
|
782
|
-
function Script({
|
|
783
|
-
src,
|
|
784
|
-
strategy = 'afterInteractive',
|
|
785
|
-
onLoad,
|
|
786
|
-
onReady,
|
|
787
|
-
onError,
|
|
788
|
-
children,
|
|
789
|
-
dangerouslySetInnerHTML,
|
|
790
|
-
...rest
|
|
791
|
-
}) {
|
|
792
|
-
React.useEffect(function() {
|
|
793
|
-
if (!src && !children && !dangerouslySetInnerHTML) return;
|
|
794
|
-
|
|
795
|
-
var script = document.createElement('script');
|
|
796
|
-
|
|
797
|
-
if (src) {
|
|
798
|
-
script.src = src;
|
|
799
|
-
script.async = strategy !== 'beforeInteractive';
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
Object.keys(rest).forEach(function(key) {
|
|
803
|
-
script.setAttribute(key, rest[key]);
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
if (children) {
|
|
807
|
-
script.textContent = children;
|
|
808
|
-
} else if (dangerouslySetInnerHTML && dangerouslySetInnerHTML.__html) {
|
|
809
|
-
script.textContent = dangerouslySetInnerHTML.__html;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
script.onload = function() {
|
|
813
|
-
if (onLoad) onLoad();
|
|
814
|
-
if (onReady) onReady();
|
|
815
|
-
};
|
|
816
|
-
script.onerror = onError;
|
|
817
|
-
|
|
818
|
-
document.head.appendChild(script);
|
|
819
|
-
|
|
820
|
-
return function() {
|
|
821
|
-
if (script.parentNode) {
|
|
822
|
-
script.parentNode.removeChild(script);
|
|
823
|
-
}
|
|
824
|
-
};
|
|
825
|
-
}, [src]);
|
|
826
|
-
|
|
827
|
-
return null;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
export default Script;
|
|
831
|
-
export { Script };
|
|
832
|
-
`;
|
|
833
|
-
|
|
834
|
-
/**
|
|
835
|
-
* next/font/google shim - Loads Google Fonts via CDN
|
|
836
|
-
* Uses a Proxy to dynamically handle ANY Google Font without hardcoding
|
|
837
|
-
*/
|
|
838
|
-
const NEXT_FONT_GOOGLE_SHIM = `
|
|
839
|
-
// Track loaded fonts to avoid duplicate style injections
|
|
840
|
-
const loadedFonts = new Set();
|
|
841
|
-
|
|
842
|
-
/**
|
|
843
|
-
* Convert font function name to Google Fonts family name
|
|
844
|
-
* Examples:
|
|
845
|
-
* DM_Sans -> DM Sans
|
|
846
|
-
* Open_Sans -> Open Sans
|
|
847
|
-
* Fraunces -> Fraunces
|
|
848
|
-
*/
|
|
849
|
-
function toFontFamily(fontName) {
|
|
850
|
-
return fontName.replace(/_/g, ' ');
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
/**
|
|
854
|
-
* Inject font CSS into document
|
|
855
|
-
* - Adds preconnect links for faster font loading
|
|
856
|
-
* - Loads the font from Google Fonts CDN
|
|
857
|
-
* - Creates a CSS class that sets the CSS variable
|
|
858
|
-
*/
|
|
859
|
-
function injectFontCSS(fontFamily, variableName, weight, style) {
|
|
860
|
-
const fontKey = fontFamily + '-' + (variableName || 'default');
|
|
861
|
-
if (loadedFonts.has(fontKey)) {
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
loadedFonts.add(fontKey);
|
|
865
|
-
|
|
866
|
-
if (typeof document === 'undefined') {
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// Add preconnect links for faster loading (only once)
|
|
871
|
-
if (!document.querySelector('link[href="https://fonts.googleapis.com"]')) {
|
|
872
|
-
const preconnect1 = document.createElement('link');
|
|
873
|
-
preconnect1.rel = 'preconnect';
|
|
874
|
-
preconnect1.href = 'https://fonts.googleapis.com';
|
|
875
|
-
document.head.appendChild(preconnect1);
|
|
876
|
-
|
|
877
|
-
const preconnect2 = document.createElement('link');
|
|
878
|
-
preconnect2.rel = 'preconnect';
|
|
879
|
-
preconnect2.href = 'https://fonts.gstatic.com';
|
|
880
|
-
preconnect2.crossOrigin = 'anonymous';
|
|
881
|
-
document.head.appendChild(preconnect2);
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
// Build Google Fonts URL
|
|
885
|
-
const escapedFamily = fontFamily.replace(/ /g, '+');
|
|
886
|
-
|
|
887
|
-
// Build axis list based on options
|
|
888
|
-
let axisList = '';
|
|
889
|
-
const axes = [];
|
|
890
|
-
|
|
891
|
-
// Handle italic style
|
|
892
|
-
if (style === 'italic') {
|
|
893
|
-
axes.push('ital');
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// Handle weight - use specific weight or variable range
|
|
897
|
-
if (weight && weight !== '400' && !Array.isArray(weight)) {
|
|
898
|
-
// Specific weight requested
|
|
899
|
-
axes.push('wght');
|
|
900
|
-
if (style === 'italic') {
|
|
901
|
-
axisList = ':ital,wght@1,' + weight;
|
|
902
|
-
} else {
|
|
903
|
-
axisList = ':wght@' + weight;
|
|
904
|
-
}
|
|
905
|
-
} else if (Array.isArray(weight)) {
|
|
906
|
-
// Multiple weights
|
|
907
|
-
axes.push('wght');
|
|
908
|
-
axisList = ':wght@' + weight.join(';');
|
|
909
|
-
} else {
|
|
910
|
-
// Default: request common weights for flexibility
|
|
911
|
-
axisList = ':wght@400;500;600;700';
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
const fontUrl = 'https://fonts.googleapis.com/css2?family=' +
|
|
915
|
-
escapedFamily + axisList + '&display=swap';
|
|
916
|
-
|
|
917
|
-
// Add link element for Google Fonts (if not already present)
|
|
918
|
-
if (!document.querySelector('link[href*="family=' + escapedFamily + '"]')) {
|
|
919
|
-
const link = document.createElement('link');
|
|
920
|
-
link.rel = 'stylesheet';
|
|
921
|
-
link.href = fontUrl;
|
|
922
|
-
document.head.appendChild(link);
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// Create style element for CSS variable at :root level (globally available)
|
|
926
|
-
// This makes the variable work without needing to apply the class to body
|
|
927
|
-
if (variableName) {
|
|
928
|
-
const styleEl = document.createElement('style');
|
|
929
|
-
styleEl.setAttribute('data-font-var', variableName);
|
|
930
|
-
styleEl.textContent = ':root { ' + variableName + ': "' + fontFamily + '", ' + (fontFamily.includes('Serif') ? 'serif' : 'sans-serif') + '; }';
|
|
931
|
-
document.head.appendChild(styleEl);
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
/**
|
|
936
|
-
* Create a font loader function for a specific font
|
|
937
|
-
*/
|
|
938
|
-
function createFontLoader(fontName) {
|
|
939
|
-
const fontFamily = toFontFamily(fontName);
|
|
940
|
-
|
|
941
|
-
return function(options = {}) {
|
|
942
|
-
const {
|
|
943
|
-
weight,
|
|
944
|
-
style = 'normal',
|
|
945
|
-
subsets = ['latin'],
|
|
946
|
-
variable,
|
|
947
|
-
display = 'swap',
|
|
948
|
-
preload = true,
|
|
949
|
-
fallback = ['sans-serif'],
|
|
950
|
-
adjustFontFallback = true
|
|
951
|
-
} = options;
|
|
952
|
-
|
|
953
|
-
// Inject the font CSS
|
|
954
|
-
injectFontCSS(fontFamily, variable, weight, style);
|
|
955
|
-
|
|
956
|
-
// Generate class name from variable (--font-inter -> __font-inter)
|
|
957
|
-
const className = variable
|
|
958
|
-
? variable.replace('--', '__')
|
|
959
|
-
: '__font-' + fontName.toLowerCase().replace(/_/g, '-');
|
|
960
|
-
|
|
961
|
-
return {
|
|
962
|
-
className,
|
|
963
|
-
variable: className,
|
|
964
|
-
style: {
|
|
965
|
-
fontFamily: '"' + fontFamily + '", ' + fallback.join(', ')
|
|
966
|
-
}
|
|
967
|
-
};
|
|
968
|
-
};
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
/**
|
|
972
|
-
* Use a Proxy to dynamically create font loaders for ANY font name
|
|
973
|
-
* This allows: import { AnyGoogleFont } from "next/font/google"
|
|
974
|
-
*/
|
|
975
|
-
const fontProxy = new Proxy({}, {
|
|
976
|
-
get(target, prop) {
|
|
977
|
-
// Handle special properties
|
|
978
|
-
if (prop === '__esModule') return true;
|
|
979
|
-
if (prop === 'default') return fontProxy;
|
|
980
|
-
if (typeof prop !== 'string') return undefined;
|
|
981
|
-
|
|
982
|
-
// Create a font loader for this font name
|
|
983
|
-
return createFontLoader(prop);
|
|
984
|
-
}
|
|
985
|
-
});
|
|
986
|
-
|
|
987
|
-
// Export the proxy as both default and named exports
|
|
988
|
-
export default fontProxy;
|
|
989
|
-
|
|
990
|
-
// Re-export through proxy for named imports
|
|
991
|
-
export const {
|
|
992
|
-
Fraunces, Inter, DM_Sans, DM_Serif_Text, Roboto, Open_Sans, Lato,
|
|
993
|
-
Montserrat, Poppins, Playfair_Display, Merriweather, Raleway, Nunito,
|
|
994
|
-
Ubuntu, Oswald, Quicksand, Work_Sans, Fira_Sans, Barlow, Mulish, Rubik,
|
|
995
|
-
Noto_Sans, Manrope, Space_Grotesk, Geist, Geist_Mono
|
|
996
|
-
} = fontProxy;
|
|
997
|
-
`;
|
|
998
|
-
|
|
999
112
|
/**
|
|
1000
113
|
* NextDevServer - A lightweight Next.js-compatible development server
|
|
1001
114
|
*
|
|
@@ -1056,6 +169,9 @@ export class NextDevServer extends DevServer {
|
|
|
1056
169
|
/** Asset prefix for static files (e.g., '/marketing') */
|
|
1057
170
|
private assetPrefix: string = '';
|
|
1058
171
|
|
|
172
|
+
/** Base path for the app (e.g., '/docs') */
|
|
173
|
+
private basePath: string = '';
|
|
174
|
+
|
|
1059
175
|
constructor(vfs: VirtualFS, options: NextDevServerOptions) {
|
|
1060
176
|
super(vfs, options);
|
|
1061
177
|
this.options = options;
|
|
@@ -1077,6 +193,9 @@ export class NextDevServer extends DevServer {
|
|
|
1077
193
|
|
|
1078
194
|
// Load assetPrefix from options or auto-detect from next.config
|
|
1079
195
|
this.loadAssetPrefix(options.assetPrefix);
|
|
196
|
+
|
|
197
|
+
// Load basePath from options or auto-detect from next.config
|
|
198
|
+
this.loadBasePath(options.basePath);
|
|
1080
199
|
}
|
|
1081
200
|
|
|
1082
201
|
/**
|
|
@@ -1114,50 +233,45 @@ export class NextDevServer extends DevServer {
|
|
|
1114
233
|
}
|
|
1115
234
|
|
|
1116
235
|
/**
|
|
1117
|
-
* Load
|
|
1118
|
-
* The assetPrefix is used to prefix static asset URLs (e.g., '/marketing')
|
|
236
|
+
* Load a string config value from options or auto-detect from next.config.ts/js
|
|
1119
237
|
*/
|
|
1120
|
-
private
|
|
1121
|
-
// If explicitly provided in options, use it
|
|
238
|
+
private loadConfigStringValue(key: string, optionValue?: string): string {
|
|
1122
239
|
if (optionValue !== undefined) {
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
this.assetPrefix = this.assetPrefix.slice(0, -1);
|
|
1127
|
-
}
|
|
1128
|
-
return;
|
|
240
|
+
let val = optionValue.startsWith('/') ? optionValue : `/${optionValue}`;
|
|
241
|
+
if (val.endsWith('/')) val = val.slice(0, -1);
|
|
242
|
+
return val;
|
|
1129
243
|
}
|
|
1130
244
|
|
|
1131
|
-
// Try to auto-detect from next.config.ts or next.config.js
|
|
1132
245
|
try {
|
|
1133
|
-
const configFiles
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
// Normalize: ensure it starts with / and doesn't end with /
|
|
1148
|
-
if (!prefix.startsWith('/')) {
|
|
1149
|
-
prefix = `/${prefix}`;
|
|
1150
|
-
}
|
|
1151
|
-
if (prefix.endsWith('/')) {
|
|
1152
|
-
prefix = prefix.slice(0, -1);
|
|
1153
|
-
}
|
|
1154
|
-
this.assetPrefix = prefix;
|
|
1155
|
-
return;
|
|
246
|
+
const configFiles: { path: string; isTs: boolean }[] = [
|
|
247
|
+
{ path: '/next.config.ts', isTs: true },
|
|
248
|
+
{ path: '/next.config.js', isTs: false },
|
|
249
|
+
{ path: '/next.config.mjs', isTs: false },
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
for (const { path, isTs } of configFiles) {
|
|
253
|
+
if (!this.vfs.existsSync(path)) continue;
|
|
254
|
+
const content = this.vfs.readFileSync(path, 'utf-8');
|
|
255
|
+
const value = parseNextConfigValue(content, key, isTs);
|
|
256
|
+
if (value) {
|
|
257
|
+
let normalized = value.startsWith('/') ? value : `/${value}`;
|
|
258
|
+
if (normalized.endsWith('/')) normalized = normalized.slice(0, -1);
|
|
259
|
+
return normalized;
|
|
1156
260
|
}
|
|
1157
261
|
}
|
|
1158
|
-
} catch
|
|
262
|
+
} catch {
|
|
1159
263
|
// Silently ignore config parse errors
|
|
1160
264
|
}
|
|
265
|
+
|
|
266
|
+
return '';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private loadAssetPrefix(optionValue?: string): void {
|
|
270
|
+
this.assetPrefix = this.loadConfigStringValue('assetPrefix', optionValue);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private loadBasePath(optionValue?: string): void {
|
|
274
|
+
this.basePath = this.loadConfigStringValue('basePath', optionValue);
|
|
1161
275
|
}
|
|
1162
276
|
|
|
1163
277
|
/**
|
|
@@ -1246,6 +360,8 @@ export class NextDevServer extends DevServer {
|
|
|
1246
360
|
window.process = window.process || {};
|
|
1247
361
|
window.process.env = window.process.env || {};
|
|
1248
362
|
Object.assign(window.process.env, ${JSON.stringify(publicEnvVars)});
|
|
363
|
+
// Next.js config values
|
|
364
|
+
window.__NEXT_BASE_PATH__ = ${JSON.stringify(this.basePath)};
|
|
1249
365
|
</script>`;
|
|
1250
366
|
}
|
|
1251
367
|
|
|
@@ -1285,11 +401,30 @@ export class NextDevServer extends DevServer {
|
|
|
1285
401
|
// Check if /app directory exists and has a page file
|
|
1286
402
|
if (!this.exists(this.appDir)) return false;
|
|
1287
403
|
|
|
1288
|
-
// Check for root page
|
|
1289
404
|
const extensions = ['.jsx', '.tsx', '.js', '.ts'];
|
|
405
|
+
|
|
406
|
+
// Check for root page directly
|
|
1290
407
|
for (const ext of extensions) {
|
|
1291
408
|
if (this.exists(`${this.appDir}/page${ext}`)) return true;
|
|
1292
409
|
}
|
|
410
|
+
|
|
411
|
+
// Check for root page inside route groups (e.g., /app/(main)/page.tsx)
|
|
412
|
+
try {
|
|
413
|
+
const entries = this.vfs.readdirSync(this.appDir);
|
|
414
|
+
for (const entry of entries) {
|
|
415
|
+
if (/^\([^)]+\)$/.test(entry) && this.isDirectory(`${this.appDir}/${entry}`)) {
|
|
416
|
+
for (const ext of extensions) {
|
|
417
|
+
if (this.exists(`${this.appDir}/${entry}/page${ext}`)) return true;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
} catch { /* ignore */ }
|
|
422
|
+
|
|
423
|
+
// Also check for any layout.tsx which indicates App Router usage
|
|
424
|
+
for (const ext of extensions) {
|
|
425
|
+
if (this.exists(`${this.appDir}/layout${ext}`)) return true;
|
|
426
|
+
}
|
|
427
|
+
|
|
1293
428
|
return false;
|
|
1294
429
|
} catch {
|
|
1295
430
|
return false;
|
|
@@ -1329,6 +464,14 @@ export class NextDevServer extends DevServer {
|
|
|
1329
464
|
}
|
|
1330
465
|
}
|
|
1331
466
|
|
|
467
|
+
// Strip basePath if present (e.g., /docs/about -> /about)
|
|
468
|
+
if (this.basePath && pathname.startsWith(this.basePath)) {
|
|
469
|
+
const rest = pathname.slice(this.basePath.length);
|
|
470
|
+
if (rest === '' || rest.startsWith('/')) {
|
|
471
|
+
pathname = rest || '/';
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
1332
475
|
// Serve Next.js shims
|
|
1333
476
|
if (pathname.startsWith('/_next/shims/')) {
|
|
1334
477
|
return this.serveNextShim(pathname);
|
|
@@ -1354,7 +497,15 @@ export class NextDevServer extends DevServer {
|
|
|
1354
497
|
return this.serveStaticAsset(pathname);
|
|
1355
498
|
}
|
|
1356
499
|
|
|
1357
|
-
// API routes
|
|
500
|
+
// App Router API routes (route.ts/route.js) - check before Pages Router API routes
|
|
501
|
+
if (this.useAppRouter) {
|
|
502
|
+
const appRouteFile = this.resolveAppRouteHandler(pathname);
|
|
503
|
+
if (appRouteFile) {
|
|
504
|
+
return this.handleAppRouteHandler(method, pathname, headers, body, appRouteFile, urlObj.search);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Pages Router API routes: /api/*
|
|
1358
509
|
if (pathname.startsWith('/api/')) {
|
|
1359
510
|
return this.handleApiRoute(method, pathname, headers, body);
|
|
1360
511
|
}
|
|
@@ -1421,6 +572,9 @@ export class NextDevServer extends DevServer {
|
|
|
1421
572
|
case 'font/google':
|
|
1422
573
|
code = NEXT_FONT_GOOGLE_SHIM;
|
|
1423
574
|
break;
|
|
575
|
+
case 'font/local':
|
|
576
|
+
code = NEXT_FONT_LOCAL_SHIM;
|
|
577
|
+
break;
|
|
1424
578
|
default:
|
|
1425
579
|
return this.notFound(pathname);
|
|
1426
580
|
}
|
|
@@ -1503,12 +657,18 @@ export class NextDevServer extends DevServer {
|
|
|
1503
657
|
* Maps /_next/app/app/about/page.js → /app/about/page.tsx (transformed)
|
|
1504
658
|
*/
|
|
1505
659
|
private async serveAppComponent(pathname: string): Promise<ResponseData> {
|
|
1506
|
-
// Extract the file path from /_next/app
|
|
1507
|
-
const
|
|
1508
|
-
|
|
1509
|
-
|
|
660
|
+
// Extract the file path from /_next/app prefix
|
|
661
|
+
const rawFilePath = pathname.replace('/_next/app', '');
|
|
662
|
+
|
|
663
|
+
// First, try the path as-is (handles imports with explicit extensions like .tsx/.ts)
|
|
664
|
+
if (this.exists(rawFilePath) && !this.isDirectory(rawFilePath)) {
|
|
665
|
+
return this.transformAndServe(rawFilePath, rawFilePath);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Strip .js extension and try different extensions
|
|
669
|
+
// e.g. /_next/app/app/about/page.js → /app/about/page → /app/about/page.tsx
|
|
670
|
+
const filePath = rawFilePath.replace(/\.js$/, '');
|
|
1510
671
|
|
|
1511
|
-
// Try different extensions
|
|
1512
672
|
const extensions = ['.tsx', '.jsx', '.ts', '.js'];
|
|
1513
673
|
for (const ext of extensions) {
|
|
1514
674
|
const fullPath = filePath + ext;
|
|
@@ -1539,32 +699,264 @@ export class NextDevServer extends DevServer {
|
|
|
1539
699
|
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
1540
700
|
body: Buffer.from(JSON.stringify({ error: 'API route not found' })),
|
|
1541
701
|
};
|
|
1542
|
-
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
try {
|
|
705
|
+
// Read and transform the API handler to CJS for eval execution
|
|
706
|
+
const code = this.vfs.readFileSync(apiFile, 'utf8');
|
|
707
|
+
const transformed = await this.transformApiHandler(code, apiFile);
|
|
708
|
+
|
|
709
|
+
// Create mock req/res objects
|
|
710
|
+
const req = this.createMockRequest(method, pathname, headers, body);
|
|
711
|
+
const res = this.createMockResponse();
|
|
712
|
+
|
|
713
|
+
// Execute the handler
|
|
714
|
+
await this.executeApiHandler(transformed, req, res);
|
|
715
|
+
|
|
716
|
+
// Wait for async handlers (like those using https.get with callbacks)
|
|
717
|
+
// with a reasonable timeout
|
|
718
|
+
if (!res.isEnded()) {
|
|
719
|
+
const timeout = new Promise<void>((_, reject) => {
|
|
720
|
+
setTimeout(() => reject(new Error('API handler timeout')), 30000);
|
|
721
|
+
});
|
|
722
|
+
await Promise.race([res.waitForEnd(), timeout]);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return res.toResponse();
|
|
726
|
+
} catch (error) {
|
|
727
|
+
console.error('[NextDevServer] API error:', error);
|
|
728
|
+
return {
|
|
729
|
+
statusCode: 500,
|
|
730
|
+
statusMessage: 'Internal Server Error',
|
|
731
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
732
|
+
body: Buffer.from(JSON.stringify({
|
|
733
|
+
error: error instanceof Error ? error.message : 'Internal Server Error'
|
|
734
|
+
})),
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Resolve an App Router route handler (route.ts/route.js)
|
|
741
|
+
* Returns the file path if found, null otherwise
|
|
742
|
+
*/
|
|
743
|
+
private resolveAppRouteHandler(pathname: string): string | null {
|
|
744
|
+
const extensions = ['.ts', '.js', '.tsx', '.jsx'];
|
|
745
|
+
|
|
746
|
+
// Build the directory path in the app dir
|
|
747
|
+
const segments = pathname === '/' ? [] : pathname.split('/').filter(Boolean);
|
|
748
|
+
let dirPath = this.appDir;
|
|
749
|
+
|
|
750
|
+
for (const segment of segments) {
|
|
751
|
+
dirPath = `${dirPath}/${segment}`;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Check for route file
|
|
755
|
+
for (const ext of extensions) {
|
|
756
|
+
const routePath = `${dirPath}/route${ext}`;
|
|
757
|
+
if (this.exists(routePath)) {
|
|
758
|
+
return routePath;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Try dynamic route resolution with route groups
|
|
763
|
+
return this.resolveAppRouteHandlerDynamic(segments);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Resolve dynamic App Router route handlers with route group support
|
|
768
|
+
*/
|
|
769
|
+
private resolveAppRouteHandlerDynamic(segments: string[]): string | null {
|
|
770
|
+
const extensions = ['.ts', '.js', '.tsx', '.jsx'];
|
|
771
|
+
|
|
772
|
+
const tryPath = (dirPath: string, remainingSegments: string[]): string | null => {
|
|
773
|
+
if (remainingSegments.length === 0) {
|
|
774
|
+
for (const ext of extensions) {
|
|
775
|
+
const routePath = `${dirPath}/route${ext}`;
|
|
776
|
+
if (this.exists(routePath)) {
|
|
777
|
+
return routePath;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Check route groups
|
|
782
|
+
try {
|
|
783
|
+
const entries = this.vfs.readdirSync(dirPath);
|
|
784
|
+
for (const entry of entries) {
|
|
785
|
+
if (/^\([^)]+\)$/.test(entry) && this.isDirectory(`${dirPath}/${entry}`)) {
|
|
786
|
+
for (const ext of extensions) {
|
|
787
|
+
const routePath = `${dirPath}/${entry}/route${ext}`;
|
|
788
|
+
if (this.exists(routePath)) {
|
|
789
|
+
return routePath;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
} catch { /* ignore */ }
|
|
795
|
+
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const [current, ...rest] = remainingSegments;
|
|
800
|
+
|
|
801
|
+
// Try exact match
|
|
802
|
+
const exactPath = `${dirPath}/${current}`;
|
|
803
|
+
if (this.isDirectory(exactPath)) {
|
|
804
|
+
const result = tryPath(exactPath, rest);
|
|
805
|
+
if (result) return result;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Try route groups and dynamic segments
|
|
809
|
+
try {
|
|
810
|
+
const entries = this.vfs.readdirSync(dirPath);
|
|
811
|
+
for (const entry of entries) {
|
|
812
|
+
// Route groups
|
|
813
|
+
if (/^\([^)]+\)$/.test(entry) && this.isDirectory(`${dirPath}/${entry}`)) {
|
|
814
|
+
const groupExact = `${dirPath}/${entry}/${current}`;
|
|
815
|
+
if (this.isDirectory(groupExact)) {
|
|
816
|
+
const result = tryPath(groupExact, rest);
|
|
817
|
+
if (result) return result;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
// Dynamic segments
|
|
821
|
+
if (entry.startsWith('[') && entry.endsWith(']') && !entry.includes('.')) {
|
|
822
|
+
const dynamicPath = `${dirPath}/${entry}`;
|
|
823
|
+
if (this.isDirectory(dynamicPath)) {
|
|
824
|
+
const result = tryPath(dynamicPath, rest);
|
|
825
|
+
if (result) return result;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// Catch-all
|
|
829
|
+
if (entry.startsWith('[...') && entry.endsWith(']')) {
|
|
830
|
+
const dynamicPath = `${dirPath}/${entry}`;
|
|
831
|
+
if (this.isDirectory(dynamicPath)) {
|
|
832
|
+
const result = tryPath(dynamicPath, []);
|
|
833
|
+
if (result) return result;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
} catch { /* ignore */ }
|
|
838
|
+
|
|
839
|
+
return null;
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
return tryPath(this.appDir, segments);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Handle App Router route handler (route.ts) requests
|
|
847
|
+
* These use the Web Request/Response API pattern
|
|
848
|
+
*/
|
|
849
|
+
private async handleAppRouteHandler(
|
|
850
|
+
method: string,
|
|
851
|
+
pathname: string,
|
|
852
|
+
headers: Record<string, string>,
|
|
853
|
+
body: Buffer | undefined,
|
|
854
|
+
routeFile: string,
|
|
855
|
+
search?: string
|
|
856
|
+
): Promise<ResponseData> {
|
|
857
|
+
try {
|
|
858
|
+
const code = this.vfs.readFileSync(routeFile, 'utf8');
|
|
859
|
+
const transformed = await this.transformApiHandler(code, routeFile);
|
|
860
|
+
|
|
861
|
+
// Create module context
|
|
862
|
+
const builtinModules: Record<string, unknown> = {
|
|
863
|
+
https: await import('../shims/https'),
|
|
864
|
+
http: await import('../shims/http'),
|
|
865
|
+
path: await import('../shims/path'),
|
|
866
|
+
url: await import('../shims/url'),
|
|
867
|
+
querystring: await import('../shims/querystring'),
|
|
868
|
+
util: await import('../shims/util'),
|
|
869
|
+
events: await import('../shims/events'),
|
|
870
|
+
stream: await import('../shims/stream'),
|
|
871
|
+
buffer: await import('../shims/buffer'),
|
|
872
|
+
crypto: await import('../shims/crypto'),
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
const require = (id: string): unknown => {
|
|
876
|
+
const modId = id.startsWith('node:') ? id.slice(5) : id;
|
|
877
|
+
if (builtinModules[modId]) return builtinModules[modId];
|
|
878
|
+
throw new Error(`Module not found: ${id}`);
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
const module = { exports: {} as Record<string, unknown> };
|
|
882
|
+
const exports = module.exports;
|
|
883
|
+
const process = {
|
|
884
|
+
env: { ...this.options.env },
|
|
885
|
+
cwd: () => '/',
|
|
886
|
+
platform: 'browser',
|
|
887
|
+
version: 'v18.0.0',
|
|
888
|
+
versions: { node: '18.0.0' },
|
|
889
|
+
};
|
|
1543
890
|
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
const code = this.vfs.readFileSync(apiFile, 'utf8');
|
|
1547
|
-
const transformed = await this.transformApiHandler(code, apiFile);
|
|
891
|
+
const fn = new Function('exports', 'require', 'module', 'process', transformed);
|
|
892
|
+
fn(exports, require, module, process);
|
|
1548
893
|
|
|
1549
|
-
//
|
|
1550
|
-
const
|
|
1551
|
-
const
|
|
894
|
+
// Get the handler for the HTTP method
|
|
895
|
+
const methodUpper = method.toUpperCase();
|
|
896
|
+
const handler = module.exports[methodUpper] || module.exports[methodUpper.toLowerCase()];
|
|
1552
897
|
|
|
1553
|
-
|
|
1554
|
-
|
|
898
|
+
if (typeof handler !== 'function') {
|
|
899
|
+
return {
|
|
900
|
+
statusCode: 405,
|
|
901
|
+
statusMessage: 'Method Not Allowed',
|
|
902
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
903
|
+
body: Buffer.from(JSON.stringify({ error: `Method ${method} not allowed` })),
|
|
904
|
+
};
|
|
905
|
+
}
|
|
1555
906
|
|
|
1556
|
-
//
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
907
|
+
// Create a Web API Request object
|
|
908
|
+
const requestUrl = new URL(pathname + (search || ''), 'http://localhost');
|
|
909
|
+
const requestInit: RequestInit = {
|
|
910
|
+
method: methodUpper,
|
|
911
|
+
headers: new Headers(headers),
|
|
912
|
+
};
|
|
913
|
+
if (body && methodUpper !== 'GET' && methodUpper !== 'HEAD') {
|
|
914
|
+
requestInit.body = body;
|
|
915
|
+
}
|
|
916
|
+
const request = new Request(requestUrl.toString(), requestInit);
|
|
917
|
+
|
|
918
|
+
// Extract route params
|
|
919
|
+
const route = this.resolveAppRoute(pathname);
|
|
920
|
+
const params = route?.params || {};
|
|
921
|
+
|
|
922
|
+
// Call the handler
|
|
923
|
+
const response = await handler(request, { params: Promise.resolve(params) });
|
|
924
|
+
|
|
925
|
+
// Convert Response to our format
|
|
926
|
+
if (response instanceof Response) {
|
|
927
|
+
const respHeaders: Record<string, string> = {};
|
|
928
|
+
response.headers.forEach((value: string, key: string) => {
|
|
929
|
+
respHeaders[key] = value;
|
|
1561
930
|
});
|
|
1562
|
-
|
|
931
|
+
|
|
932
|
+
const respBody = await response.text();
|
|
933
|
+
return {
|
|
934
|
+
statusCode: response.status,
|
|
935
|
+
statusMessage: response.statusText || 'OK',
|
|
936
|
+
headers: respHeaders,
|
|
937
|
+
body: Buffer.from(respBody),
|
|
938
|
+
};
|
|
1563
939
|
}
|
|
1564
940
|
|
|
1565
|
-
|
|
941
|
+
// If the handler returned a plain object, serialize as JSON
|
|
942
|
+
if (response && typeof response === 'object') {
|
|
943
|
+
const json = JSON.stringify(response);
|
|
944
|
+
return {
|
|
945
|
+
statusCode: 200,
|
|
946
|
+
statusMessage: 'OK',
|
|
947
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
948
|
+
body: Buffer.from(json),
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
return {
|
|
953
|
+
statusCode: 200,
|
|
954
|
+
statusMessage: 'OK',
|
|
955
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
956
|
+
body: Buffer.from(String(response || '')),
|
|
957
|
+
};
|
|
1566
958
|
} catch (error) {
|
|
1567
|
-
console.error('[NextDevServer]
|
|
959
|
+
console.error('[NextDevServer] App Route handler error:', error);
|
|
1568
960
|
return {
|
|
1569
961
|
statusCode: 500,
|
|
1570
962
|
statusMessage: 'Internal Server Error',
|
|
@@ -2077,82 +1469,128 @@ export class NextDevServer extends DevServer {
|
|
|
2077
1469
|
/**
|
|
2078
1470
|
* Resolve App Router route to page and layout files
|
|
2079
1471
|
*/
|
|
2080
|
-
private resolveAppRoute(pathname: string):
|
|
2081
|
-
const extensions = ['.jsx', '.tsx', '.js', '.ts'];
|
|
1472
|
+
private resolveAppRoute(pathname: string): AppRoute | null {
|
|
2082
1473
|
const segments = pathname === '/' ? [] : pathname.split('/').filter(Boolean);
|
|
1474
|
+
// Use the unified dynamic resolver which handles static, dynamic, and route groups
|
|
1475
|
+
return this.resolveAppDynamicRoute(pathname, segments);
|
|
1476
|
+
}
|
|
2083
1477
|
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
1478
|
+
/**
|
|
1479
|
+
* Resolve App Router routes including static, dynamic, and route groups.
|
|
1480
|
+
* Route groups are folders wrapped in parentheses like (marketing) that
|
|
1481
|
+
* don't affect the URL path but can have their own layouts.
|
|
1482
|
+
*/
|
|
1483
|
+
private resolveAppDynamicRoute(
|
|
1484
|
+
_pathname: string,
|
|
1485
|
+
segments: string[]
|
|
1486
|
+
): AppRoute | null {
|
|
1487
|
+
const extensions = ['.jsx', '.tsx', '.js', '.ts'];
|
|
2087
1488
|
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
1489
|
+
/**
|
|
1490
|
+
* Collect layout from a directory if it exists
|
|
1491
|
+
*/
|
|
1492
|
+
const collectLayout = (dirPath: string, layouts: string[]): string[] => {
|
|
1493
|
+
for (const ext of extensions) {
|
|
1494
|
+
const layoutPath = `${dirPath}/layout${ext}`;
|
|
1495
|
+
if (this.exists(layoutPath) && !layouts.includes(layoutPath)) {
|
|
1496
|
+
return [...layouts, layoutPath];
|
|
1497
|
+
}
|
|
2094
1498
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
// Walk through segments to find page and collect layouts
|
|
2098
|
-
for (const segment of segments) {
|
|
2099
|
-
dirPath = `${dirPath}/${segment}`;
|
|
1499
|
+
return layouts;
|
|
1500
|
+
};
|
|
2100
1501
|
|
|
2101
|
-
|
|
1502
|
+
/**
|
|
1503
|
+
* Find page file in a directory
|
|
1504
|
+
*/
|
|
1505
|
+
const findPage = (dirPath: string): string | null => {
|
|
2102
1506
|
for (const ext of extensions) {
|
|
2103
|
-
const
|
|
2104
|
-
if (this.exists(
|
|
2105
|
-
|
|
2106
|
-
break;
|
|
1507
|
+
const pagePath = `${dirPath}/page${ext}`;
|
|
1508
|
+
if (this.exists(pagePath)) {
|
|
1509
|
+
return pagePath;
|
|
2107
1510
|
}
|
|
2108
1511
|
}
|
|
2109
|
-
|
|
1512
|
+
return null;
|
|
1513
|
+
};
|
|
2110
1514
|
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
1515
|
+
/**
|
|
1516
|
+
* Find a UI convention file (loading, error, not-found) in a directory
|
|
1517
|
+
*/
|
|
1518
|
+
const findConventionFile = (dirPath: string, name: string): string | null => {
|
|
1519
|
+
for (const ext of extensions) {
|
|
1520
|
+
const filePath = `${dirPath}/${name}${ext}`;
|
|
1521
|
+
if (this.exists(filePath)) {
|
|
1522
|
+
return filePath;
|
|
1523
|
+
}
|
|
2117
1524
|
}
|
|
2118
|
-
|
|
1525
|
+
return null;
|
|
1526
|
+
};
|
|
2119
1527
|
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
1528
|
+
/**
|
|
1529
|
+
* Find the nearest convention file by walking up from the page directory
|
|
1530
|
+
*/
|
|
1531
|
+
const findNearestConventionFile = (dirPath: string, name: string): string | null => {
|
|
1532
|
+
let current = dirPath;
|
|
1533
|
+
while (current.startsWith(this.appDir)) {
|
|
1534
|
+
const file = findConventionFile(current, name);
|
|
1535
|
+
if (file) return file;
|
|
1536
|
+
// Move up one directory
|
|
1537
|
+
const parent = current.replace(/\/[^/]+$/, '');
|
|
1538
|
+
if (parent === current) break;
|
|
1539
|
+
current = parent;
|
|
1540
|
+
}
|
|
1541
|
+
return null;
|
|
1542
|
+
};
|
|
2123
1543
|
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
1544
|
+
/**
|
|
1545
|
+
* Get route group directories (folders matching (name) pattern)
|
|
1546
|
+
*/
|
|
1547
|
+
const getRouteGroups = (dirPath: string): string[] => {
|
|
1548
|
+
try {
|
|
1549
|
+
const entries = this.vfs.readdirSync(dirPath);
|
|
1550
|
+
return entries.filter(e => /^\([^)]+\)$/.test(e) && this.isDirectory(`${dirPath}/${e}`));
|
|
1551
|
+
} catch {
|
|
1552
|
+
return [];
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
2133
1555
|
|
|
2134
1556
|
const tryPath = (
|
|
2135
1557
|
dirPath: string,
|
|
2136
1558
|
remainingSegments: string[],
|
|
2137
1559
|
layouts: string[],
|
|
2138
1560
|
params: Record<string, string | string[]>
|
|
2139
|
-
):
|
|
1561
|
+
): AppRoute | null => {
|
|
2140
1562
|
// 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
|
-
}
|
|
1563
|
+
layouts = collectLayout(dirPath, layouts);
|
|
2147
1564
|
|
|
2148
1565
|
if (remainingSegments.length === 0) {
|
|
2149
|
-
// Look for page file
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
1566
|
+
// Look for page file directly
|
|
1567
|
+
const page = findPage(dirPath);
|
|
1568
|
+
if (page) {
|
|
1569
|
+
return {
|
|
1570
|
+
page, layouts, params,
|
|
1571
|
+
loading: findNearestConventionFile(dirPath, 'loading') || undefined,
|
|
1572
|
+
error: findNearestConventionFile(dirPath, 'error') || undefined,
|
|
1573
|
+
notFound: findNearestConventionFile(dirPath, 'not-found') || undefined,
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// Look for page inside route groups at this level
|
|
1578
|
+
// e.g., /app/(marketing)/page.tsx resolves to /
|
|
1579
|
+
const groups = getRouteGroups(dirPath);
|
|
1580
|
+
for (const group of groups) {
|
|
1581
|
+
const groupPath = `${dirPath}/${group}`;
|
|
1582
|
+
const groupLayouts = collectLayout(groupPath, layouts);
|
|
1583
|
+
const page = findPage(groupPath);
|
|
1584
|
+
if (page) {
|
|
1585
|
+
return {
|
|
1586
|
+
page, layouts: groupLayouts, params,
|
|
1587
|
+
loading: findNearestConventionFile(groupPath, 'loading') || undefined,
|
|
1588
|
+
error: findNearestConventionFile(groupPath, 'error') || undefined,
|
|
1589
|
+
notFound: findNearestConventionFile(groupPath, 'not-found') || undefined,
|
|
1590
|
+
};
|
|
2154
1591
|
}
|
|
2155
1592
|
}
|
|
1593
|
+
|
|
2156
1594
|
return null;
|
|
2157
1595
|
}
|
|
2158
1596
|
|
|
@@ -2165,7 +1603,56 @@ export class NextDevServer extends DevServer {
|
|
|
2165
1603
|
if (result) return result;
|
|
2166
1604
|
}
|
|
2167
1605
|
|
|
2168
|
-
// Try
|
|
1606
|
+
// Try inside route groups - route groups are transparent in URL
|
|
1607
|
+
// e.g., /about might match /app/(marketing)/about/page.tsx
|
|
1608
|
+
const groups = getRouteGroups(dirPath);
|
|
1609
|
+
for (const group of groups) {
|
|
1610
|
+
const groupPath = `${dirPath}/${group}`;
|
|
1611
|
+
const groupLayouts = collectLayout(groupPath, layouts);
|
|
1612
|
+
|
|
1613
|
+
// Try exact match inside group
|
|
1614
|
+
const groupExactPath = `${groupPath}/${current}`;
|
|
1615
|
+
if (this.isDirectory(groupExactPath)) {
|
|
1616
|
+
const result = tryPath(groupExactPath, rest, groupLayouts, params);
|
|
1617
|
+
if (result) return result;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// Try dynamic segments inside group
|
|
1621
|
+
try {
|
|
1622
|
+
const groupEntries = this.vfs.readdirSync(groupPath);
|
|
1623
|
+
for (const entry of groupEntries) {
|
|
1624
|
+
if (entry.startsWith('[...') && entry.endsWith(']')) {
|
|
1625
|
+
const dynamicPath = `${groupPath}/${entry}`;
|
|
1626
|
+
if (this.isDirectory(dynamicPath)) {
|
|
1627
|
+
const paramName = entry.slice(4, -1);
|
|
1628
|
+
const newParams = { ...params, [paramName]: [current, ...rest] };
|
|
1629
|
+
const result = tryPath(dynamicPath, [], groupLayouts, newParams);
|
|
1630
|
+
if (result) return result;
|
|
1631
|
+
}
|
|
1632
|
+
} else if (entry.startsWith('[[...') && entry.endsWith(']]')) {
|
|
1633
|
+
const dynamicPath = `${groupPath}/${entry}`;
|
|
1634
|
+
if (this.isDirectory(dynamicPath)) {
|
|
1635
|
+
const paramName = entry.slice(5, -2);
|
|
1636
|
+
const newParams = { ...params, [paramName]: [current, ...rest] };
|
|
1637
|
+
const result = tryPath(dynamicPath, [], groupLayouts, newParams);
|
|
1638
|
+
if (result) return result;
|
|
1639
|
+
}
|
|
1640
|
+
} else if (entry.startsWith('[') && entry.endsWith(']') && !entry.includes('.')) {
|
|
1641
|
+
const dynamicPath = `${groupPath}/${entry}`;
|
|
1642
|
+
if (this.isDirectory(dynamicPath)) {
|
|
1643
|
+
const paramName = entry.slice(1, -1);
|
|
1644
|
+
const newParams = { ...params, [paramName]: current };
|
|
1645
|
+
const result = tryPath(dynamicPath, rest, groupLayouts, newParams);
|
|
1646
|
+
if (result) return result;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
} catch {
|
|
1651
|
+
// Group directory read failed
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// Try dynamic segments at current level
|
|
2169
1656
|
try {
|
|
2170
1657
|
const entries = this.vfs.readdirSync(dirPath);
|
|
2171
1658
|
for (const entry of entries) {
|
|
@@ -2173,9 +1660,17 @@ export class NextDevServer extends DevServer {
|
|
|
2173
1660
|
if (entry.startsWith('[...') && entry.endsWith(']')) {
|
|
2174
1661
|
const dynamicPath = `${dirPath}/${entry}`;
|
|
2175
1662
|
if (this.isDirectory(dynamicPath)) {
|
|
2176
|
-
// Extract param name from [...slug]
|
|
2177
1663
|
const paramName = entry.slice(4, -1);
|
|
2178
|
-
|
|
1664
|
+
const newParams = { ...params, [paramName]: [current, ...rest] };
|
|
1665
|
+
const result = tryPath(dynamicPath, [], layouts, newParams);
|
|
1666
|
+
if (result) return result;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
// Handle optional catch-all routes [[...slug]]
|
|
1670
|
+
else if (entry.startsWith('[[...') && entry.endsWith(']]')) {
|
|
1671
|
+
const dynamicPath = `${dirPath}/${entry}`;
|
|
1672
|
+
if (this.isDirectory(dynamicPath)) {
|
|
1673
|
+
const paramName = entry.slice(5, -2);
|
|
2179
1674
|
const newParams = { ...params, [paramName]: [current, ...rest] };
|
|
2180
1675
|
const result = tryPath(dynamicPath, [], layouts, newParams);
|
|
2181
1676
|
if (result) return result;
|
|
@@ -2185,7 +1680,6 @@ export class NextDevServer extends DevServer {
|
|
|
2185
1680
|
else if (entry.startsWith('[') && entry.endsWith(']') && !entry.includes('.')) {
|
|
2186
1681
|
const dynamicPath = `${dirPath}/${entry}`;
|
|
2187
1682
|
if (this.isDirectory(dynamicPath)) {
|
|
2188
|
-
// Extract param name from [id]
|
|
2189
1683
|
const paramName = entry.slice(1, -1);
|
|
2190
1684
|
const newParams = { ...params, [paramName]: current };
|
|
2191
1685
|
const result = tryPath(dynamicPath, rest, layouts, newParams);
|
|
@@ -2213,302 +1707,28 @@ export class NextDevServer extends DevServer {
|
|
|
2213
1707
|
return tryPath(this.appDir, segments, layouts, {});
|
|
2214
1708
|
}
|
|
2215
1709
|
|
|
1710
|
+
/**
|
|
1711
|
+
* Build context object for HTML generation functions
|
|
1712
|
+
*/
|
|
1713
|
+
private htmlContext() {
|
|
1714
|
+
return {
|
|
1715
|
+
port: this.port,
|
|
1716
|
+
exists: (path: string) => this.exists(path),
|
|
1717
|
+
generateEnvScript: () => this.generateEnvScript(),
|
|
1718
|
+
loadTailwindConfigIfNeeded: () => this.loadTailwindConfigIfNeeded(),
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
|
|
2216
1722
|
/**
|
|
2217
1723
|
* Generate HTML for App Router with nested layouts
|
|
2218
1724
|
*/
|
|
2219
1725
|
private async generateAppRouterHtml(
|
|
2220
|
-
route:
|
|
1726
|
+
route: AppRoute,
|
|
2221
1727
|
pathname: string
|
|
2222
1728
|
): Promise<string> {
|
|
2223
|
-
|
|
2224
|
-
const virtualPrefix = `/__virtual__/${this.port}`;
|
|
2225
|
-
|
|
2226
|
-
// Check for global CSS files
|
|
2227
|
-
const globalCssLinks: string[] = [];
|
|
2228
|
-
const cssLocations = ['/app/globals.css', '/styles/globals.css', '/styles/global.css'];
|
|
2229
|
-
for (const cssPath of cssLocations) {
|
|
2230
|
-
if (this.exists(cssPath)) {
|
|
2231
|
-
globalCssLinks.push(`<link rel="stylesheet" href="${virtualPrefix}${cssPath}">`);
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
|
-
// Build the nested component structure
|
|
2236
|
-
// Layouts wrap the page from outside in
|
|
2237
|
-
const pageModulePath = virtualPrefix + route.page; // route.page already starts with /
|
|
2238
|
-
const layoutImports = route.layouts
|
|
2239
|
-
.map((layout, i) => `import Layout${i} from '${virtualPrefix}${layout}';`)
|
|
2240
|
-
.join('\n ');
|
|
2241
|
-
|
|
2242
|
-
// Build nested JSX: Layout0 > Layout1 > ... > Page
|
|
2243
|
-
let nestedJsx = 'React.createElement(Page)';
|
|
2244
|
-
for (let i = route.layouts.length - 1; i >= 0; i--) {
|
|
2245
|
-
nestedJsx = `React.createElement(Layout${i}, null, ${nestedJsx})`;
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
// Generate env script for NEXT_PUBLIC_* variables
|
|
2249
|
-
const envScript = this.generateEnvScript();
|
|
2250
|
-
|
|
2251
|
-
// Load Tailwind config if available (must be injected BEFORE CDN script)
|
|
2252
|
-
const tailwindConfigScript = await this.loadTailwindConfigIfNeeded();
|
|
2253
|
-
|
|
2254
|
-
return `<!DOCTYPE html>
|
|
2255
|
-
<html lang="en">
|
|
2256
|
-
<head>
|
|
2257
|
-
<meta charset="UTF-8">
|
|
2258
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2259
|
-
<base href="${virtualPrefix}/">
|
|
2260
|
-
<title>Next.js App</title>
|
|
2261
|
-
${envScript}
|
|
2262
|
-
${TAILWIND_CDN_SCRIPT}
|
|
2263
|
-
${tailwindConfigScript}
|
|
2264
|
-
${CORS_PROXY_SCRIPT}
|
|
2265
|
-
${globalCssLinks.join('\n ')}
|
|
2266
|
-
${REACT_REFRESH_PREAMBLE}
|
|
2267
|
-
<script type="importmap">
|
|
2268
|
-
{
|
|
2269
|
-
"imports": {
|
|
2270
|
-
"react": "https://esm.sh/react@18.2.0?dev",
|
|
2271
|
-
"react/": "https://esm.sh/react@18.2.0&dev/",
|
|
2272
|
-
"react-dom": "https://esm.sh/react-dom@18.2.0?dev",
|
|
2273
|
-
"react-dom/": "https://esm.sh/react-dom@18.2.0&dev/",
|
|
2274
|
-
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client?dev",
|
|
2275
|
-
"convex/react": "https://esm.sh/convex@1.21.0/react?external=react",
|
|
2276
|
-
"convex/server": "https://esm.sh/convex@1.21.0/server",
|
|
2277
|
-
"convex/values": "https://esm.sh/convex@1.21.0/values",
|
|
2278
|
-
"convex/_generated/api": "${virtualPrefix}/convex/_generated/api.ts",
|
|
2279
|
-
"ai": "https://esm.sh/ai@4?external=react",
|
|
2280
|
-
"ai/react": "https://esm.sh/ai@4/react?external=react",
|
|
2281
|
-
"@ai-sdk/openai": "https://esm.sh/@ai-sdk/openai@1",
|
|
2282
|
-
"next/link": "${virtualPrefix}/_next/shims/link.js",
|
|
2283
|
-
"next/router": "${virtualPrefix}/_next/shims/router.js",
|
|
2284
|
-
"next/head": "${virtualPrefix}/_next/shims/head.js",
|
|
2285
|
-
"next/navigation": "${virtualPrefix}/_next/shims/navigation.js",
|
|
2286
|
-
"next/image": "${virtualPrefix}/_next/shims/image.js",
|
|
2287
|
-
"next/dynamic": "${virtualPrefix}/_next/shims/dynamic.js",
|
|
2288
|
-
"next/script": "${virtualPrefix}/_next/shims/script.js",
|
|
2289
|
-
"next/font/google": "${virtualPrefix}/_next/shims/font/google.js"
|
|
2290
|
-
}
|
|
1729
|
+
return _generateAppRouterHtml(this.htmlContext(), route, pathname);
|
|
2291
1730
|
}
|
|
2292
|
-
</script>
|
|
2293
|
-
${HMR_CLIENT_SCRIPT}
|
|
2294
|
-
</head>
|
|
2295
|
-
<body>
|
|
2296
|
-
<div id="__next"></div>
|
|
2297
|
-
<script type="module">
|
|
2298
|
-
import React from 'react';
|
|
2299
|
-
import ReactDOM from 'react-dom/client';
|
|
2300
|
-
|
|
2301
|
-
const virtualBase = '${virtualPrefix}';
|
|
2302
|
-
|
|
2303
|
-
// Initial route params (embedded by server for initial page load)
|
|
2304
|
-
const initialRouteParams = ${JSON.stringify(route.params)};
|
|
2305
|
-
const initialPathname = '${pathname}';
|
|
2306
|
-
|
|
2307
|
-
// Route params cache for client-side navigation
|
|
2308
|
-
const routeParamsCache = new Map();
|
|
2309
|
-
routeParamsCache.set(initialPathname, initialRouteParams);
|
|
2310
|
-
|
|
2311
|
-
// Extract route params from server for client-side navigation
|
|
2312
|
-
async function extractRouteParams(pathname) {
|
|
2313
|
-
// Strip virtual base if present
|
|
2314
|
-
let route = pathname;
|
|
2315
|
-
if (route.startsWith(virtualBase)) {
|
|
2316
|
-
route = route.slice(virtualBase.length);
|
|
2317
|
-
}
|
|
2318
|
-
route = route.replace(/^\\/+/, '/') || '/';
|
|
2319
|
-
|
|
2320
|
-
// Check cache first
|
|
2321
|
-
if (routeParamsCache.has(route)) {
|
|
2322
|
-
return routeParamsCache.get(route);
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
|
-
try {
|
|
2326
|
-
const response = await fetch(virtualBase + '/_next/route-info?pathname=' + encodeURIComponent(route));
|
|
2327
|
-
const info = await response.json();
|
|
2328
|
-
routeParamsCache.set(route, info.params || {});
|
|
2329
|
-
return info.params || {};
|
|
2330
|
-
} catch (e) {
|
|
2331
|
-
console.error('[Router] Failed to extract route params:', e);
|
|
2332
|
-
return {};
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
// Convert URL path to app router page module path
|
|
2337
|
-
function getAppPageModulePath(pathname) {
|
|
2338
|
-
let route = pathname;
|
|
2339
|
-
if (route.startsWith(virtualBase)) {
|
|
2340
|
-
route = route.slice(virtualBase.length);
|
|
2341
|
-
}
|
|
2342
|
-
route = route.replace(/^\\/+/, '/') || '/';
|
|
2343
|
-
// App Router: / -> /app/page, /about -> /app/about/page
|
|
2344
|
-
const pagePath = route === '/' ? '/app/page' : '/app' + route + '/page';
|
|
2345
|
-
return virtualBase + '/_next/app' + pagePath + '.js';
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
|
-
// Get layout paths for a route
|
|
2349
|
-
function getLayoutPaths(pathname) {
|
|
2350
|
-
let route = pathname;
|
|
2351
|
-
if (route.startsWith(virtualBase)) {
|
|
2352
|
-
route = route.slice(virtualBase.length);
|
|
2353
|
-
}
|
|
2354
|
-
route = route.replace(/^\\/+/, '/') || '/';
|
|
2355
|
-
|
|
2356
|
-
// Build layout paths from root to current route
|
|
2357
|
-
const layouts = [virtualBase + '/_next/app/app/layout.js'];
|
|
2358
|
-
if (route !== '/') {
|
|
2359
|
-
const segments = route.split('/').filter(Boolean);
|
|
2360
|
-
let currentPath = '/app';
|
|
2361
|
-
for (const segment of segments) {
|
|
2362
|
-
currentPath += '/' + segment;
|
|
2363
|
-
layouts.push(virtualBase + '/_next/app' + currentPath + '/layout.js');
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
2366
|
-
return layouts;
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
// Dynamic page loader
|
|
2370
|
-
async function loadPage(pathname) {
|
|
2371
|
-
const modulePath = getAppPageModulePath(pathname);
|
|
2372
|
-
try {
|
|
2373
|
-
const module = await import(/* @vite-ignore */ modulePath);
|
|
2374
|
-
return module.default;
|
|
2375
|
-
} catch (e) {
|
|
2376
|
-
console.error('[Navigation] Failed to load page:', modulePath, e);
|
|
2377
|
-
return null;
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
|
|
2381
|
-
// Load layouts (with caching)
|
|
2382
|
-
const layoutCache = new Map();
|
|
2383
|
-
async function loadLayouts(pathname) {
|
|
2384
|
-
const layoutPaths = getLayoutPaths(pathname);
|
|
2385
|
-
const layouts = [];
|
|
2386
|
-
for (const path of layoutPaths) {
|
|
2387
|
-
if (layoutCache.has(path)) {
|
|
2388
|
-
layouts.push(layoutCache.get(path));
|
|
2389
|
-
} else {
|
|
2390
|
-
try {
|
|
2391
|
-
const module = await import(/* @vite-ignore */ path);
|
|
2392
|
-
layoutCache.set(path, module.default);
|
|
2393
|
-
layouts.push(module.default);
|
|
2394
|
-
} catch (e) {
|
|
2395
|
-
// Layout might not exist for this segment, skip
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
}
|
|
2399
|
-
return layouts;
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2402
|
-
// Wrapper for async Server Components
|
|
2403
|
-
function AsyncComponent({ component: Component, pathname, search }) {
|
|
2404
|
-
const [content, setContent] = React.useState(null);
|
|
2405
|
-
const [error, setError] = React.useState(null);
|
|
2406
|
-
|
|
2407
|
-
React.useEffect(() => {
|
|
2408
|
-
let cancelled = false;
|
|
2409
|
-
async function render() {
|
|
2410
|
-
try {
|
|
2411
|
-
// Create searchParams as a Promise (Next.js 15 pattern)
|
|
2412
|
-
const url = new URL(window.location.href);
|
|
2413
|
-
const searchParamsObj = Object.fromEntries(url.searchParams);
|
|
2414
|
-
const searchParams = Promise.resolve(searchParamsObj);
|
|
2415
|
-
|
|
2416
|
-
// Extract route params from pathname (fetches from server for dynamic routes)
|
|
2417
|
-
const routeParams = await extractRouteParams(pathname);
|
|
2418
|
-
const params = Promise.resolve(routeParams);
|
|
2419
|
-
|
|
2420
|
-
// Call component with props like Next.js does for page components
|
|
2421
|
-
const result = Component({ searchParams, params });
|
|
2422
|
-
if (result && typeof result.then === 'function') {
|
|
2423
|
-
// It's a Promise (async component)
|
|
2424
|
-
const resolved = await result;
|
|
2425
|
-
if (!cancelled) setContent(resolved);
|
|
2426
|
-
} else {
|
|
2427
|
-
// Synchronous component - result is already JSX
|
|
2428
|
-
if (!cancelled) setContent(result);
|
|
2429
|
-
}
|
|
2430
|
-
} catch (e) {
|
|
2431
|
-
console.error('[AsyncComponent] Error rendering:', e);
|
|
2432
|
-
if (!cancelled) setError(e);
|
|
2433
|
-
}
|
|
2434
|
-
}
|
|
2435
|
-
render();
|
|
2436
|
-
return () => { cancelled = true; };
|
|
2437
|
-
}, [Component, pathname, search]);
|
|
2438
|
-
|
|
2439
|
-
if (error) {
|
|
2440
|
-
return React.createElement('div', { style: { color: 'red', padding: '20px' } },
|
|
2441
|
-
'Error: ' + error.message
|
|
2442
|
-
);
|
|
2443
|
-
}
|
|
2444
|
-
if (!content) {
|
|
2445
|
-
return React.createElement('div', { style: { padding: '20px' } }, 'Loading...');
|
|
2446
|
-
}
|
|
2447
|
-
return content;
|
|
2448
|
-
}
|
|
2449
|
-
|
|
2450
|
-
// Router component
|
|
2451
|
-
function Router() {
|
|
2452
|
-
const [Page, setPage] = React.useState(null);
|
|
2453
|
-
const [layouts, setLayouts] = React.useState([]);
|
|
2454
|
-
const [path, setPath] = React.useState(window.location.pathname);
|
|
2455
|
-
const [search, setSearch] = React.useState(window.location.search);
|
|
2456
|
-
|
|
2457
|
-
React.useEffect(() => {
|
|
2458
|
-
Promise.all([loadPage(path), loadLayouts(path)]).then(([P, L]) => {
|
|
2459
|
-
if (P) setPage(() => P);
|
|
2460
|
-
setLayouts(L);
|
|
2461
|
-
});
|
|
2462
|
-
}, []);
|
|
2463
|
-
|
|
2464
|
-
React.useEffect(() => {
|
|
2465
|
-
const handleNavigation = async () => {
|
|
2466
|
-
const newPath = window.location.pathname;
|
|
2467
|
-
const newSearch = window.location.search;
|
|
2468
|
-
console.log('[Router] handleNavigation called, newPath:', newPath, 'current path:', path);
|
|
2469
1731
|
|
|
2470
|
-
// Always update search params
|
|
2471
|
-
if (newSearch !== search) {
|
|
2472
|
-
setSearch(newSearch);
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
if (newPath !== path) {
|
|
2476
|
-
console.log('[Router] Path changed, loading new page...');
|
|
2477
|
-
setPath(newPath);
|
|
2478
|
-
const [P, L] = await Promise.all([loadPage(newPath), loadLayouts(newPath)]);
|
|
2479
|
-
console.log('[Router] Page loaded:', !!P, 'Layouts:', L.length);
|
|
2480
|
-
if (P) setPage(() => P);
|
|
2481
|
-
setLayouts(L);
|
|
2482
|
-
} else {
|
|
2483
|
-
console.log('[Router] Path unchanged, skipping navigation');
|
|
2484
|
-
}
|
|
2485
|
-
};
|
|
2486
|
-
window.addEventListener('popstate', handleNavigation);
|
|
2487
|
-
console.log('[Router] Added popstate listener for path:', path);
|
|
2488
|
-
return () => window.removeEventListener('popstate', handleNavigation);
|
|
2489
|
-
}, [path, search]);
|
|
2490
|
-
|
|
2491
|
-
if (!Page) return null;
|
|
2492
|
-
|
|
2493
|
-
// Use AsyncComponent wrapper to handle async Server Components
|
|
2494
|
-
// Pass search to force re-render when query params change
|
|
2495
|
-
let content = React.createElement(AsyncComponent, { component: Page, pathname: path, search: search });
|
|
2496
|
-
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
2497
|
-
content = React.createElement(layouts[i], null, content);
|
|
2498
|
-
}
|
|
2499
|
-
return content;
|
|
2500
|
-
}
|
|
2501
|
-
|
|
2502
|
-
// Mark that we've initialized (for testing no-reload)
|
|
2503
|
-
window.__NEXT_INITIALIZED__ = Date.now();
|
|
2504
|
-
|
|
2505
|
-
ReactDOM.createRoot(document.getElementById('__next')).render(
|
|
2506
|
-
React.createElement(React.StrictMode, null, React.createElement(Router))
|
|
2507
|
-
);
|
|
2508
|
-
</script>
|
|
2509
|
-
</body>
|
|
2510
|
-
</html>`;
|
|
2511
|
-
}
|
|
2512
1732
|
|
|
2513
1733
|
/**
|
|
2514
1734
|
* Resolve URL pathname to page file
|
|
@@ -2634,175 +1854,14 @@ export class NextDevServer extends DevServer {
|
|
|
2634
1854
|
* Generate HTML shell for a page
|
|
2635
1855
|
*/
|
|
2636
1856
|
private async generatePageHtml(pageFile: string, pathname: string): Promise<string> {
|
|
2637
|
-
|
|
2638
|
-
// Without this, /pages/index.jsx would go to localhost:5173/pages/index.jsx
|
|
2639
|
-
// instead of /__virtual__/3001/pages/index.jsx
|
|
2640
|
-
const virtualPrefix = `/__virtual__/${this.port}`;
|
|
2641
|
-
const pageModulePath = virtualPrefix + pageFile; // pageFile already starts with /
|
|
2642
|
-
|
|
2643
|
-
// Check for global CSS files
|
|
2644
|
-
const globalCssLinks: string[] = [];
|
|
2645
|
-
const cssLocations = ['/styles/globals.css', '/styles/global.css', '/app/globals.css'];
|
|
2646
|
-
for (const cssPath of cssLocations) {
|
|
2647
|
-
if (this.exists(cssPath)) {
|
|
2648
|
-
globalCssLinks.push(`<link rel="stylesheet" href="${virtualPrefix}${cssPath}">`);
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
|
|
2652
|
-
// Generate env script for NEXT_PUBLIC_* variables
|
|
2653
|
-
const envScript = this.generateEnvScript();
|
|
2654
|
-
|
|
2655
|
-
// Load Tailwind config if available (must be injected BEFORE CDN script)
|
|
2656
|
-
const tailwindConfigScript = await this.loadTailwindConfigIfNeeded();
|
|
2657
|
-
|
|
2658
|
-
return `<!DOCTYPE html>
|
|
2659
|
-
<html lang="en">
|
|
2660
|
-
<head>
|
|
2661
|
-
<meta charset="UTF-8">
|
|
2662
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2663
|
-
<base href="${virtualPrefix}/">
|
|
2664
|
-
<title>Next.js App</title>
|
|
2665
|
-
${envScript}
|
|
2666
|
-
${TAILWIND_CDN_SCRIPT}
|
|
2667
|
-
${tailwindConfigScript}
|
|
2668
|
-
${CORS_PROXY_SCRIPT}
|
|
2669
|
-
${globalCssLinks.join('\n ')}
|
|
2670
|
-
${REACT_REFRESH_PREAMBLE}
|
|
2671
|
-
<script type="importmap">
|
|
2672
|
-
{
|
|
2673
|
-
"imports": {
|
|
2674
|
-
"react": "https://esm.sh/react@18.2.0?dev",
|
|
2675
|
-
"react/": "https://esm.sh/react@18.2.0&dev/",
|
|
2676
|
-
"react-dom": "https://esm.sh/react-dom@18.2.0?dev",
|
|
2677
|
-
"react-dom/": "https://esm.sh/react-dom@18.2.0&dev/",
|
|
2678
|
-
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client?dev",
|
|
2679
|
-
"next/link": "${virtualPrefix}/_next/shims/link.js",
|
|
2680
|
-
"next/router": "${virtualPrefix}/_next/shims/router.js",
|
|
2681
|
-
"next/head": "${virtualPrefix}/_next/shims/head.js",
|
|
2682
|
-
"next/navigation": "${virtualPrefix}/_next/shims/navigation.js",
|
|
2683
|
-
"next/image": "${virtualPrefix}/_next/shims/image.js",
|
|
2684
|
-
"next/dynamic": "${virtualPrefix}/_next/shims/dynamic.js",
|
|
2685
|
-
"next/script": "${virtualPrefix}/_next/shims/script.js",
|
|
2686
|
-
"next/font/google": "${virtualPrefix}/_next/shims/font/google.js"
|
|
2687
|
-
}
|
|
2688
|
-
}
|
|
2689
|
-
</script>
|
|
2690
|
-
${HMR_CLIENT_SCRIPT}
|
|
2691
|
-
</head>
|
|
2692
|
-
<body>
|
|
2693
|
-
<div id="__next"></div>
|
|
2694
|
-
<script type="module">
|
|
2695
|
-
import React from 'react';
|
|
2696
|
-
import ReactDOM from 'react-dom/client';
|
|
2697
|
-
|
|
2698
|
-
const virtualBase = '${virtualPrefix}';
|
|
2699
|
-
|
|
2700
|
-
// Convert URL path to page module path
|
|
2701
|
-
function getPageModulePath(pathname) {
|
|
2702
|
-
let route = pathname;
|
|
2703
|
-
if (route.startsWith(virtualBase)) {
|
|
2704
|
-
route = route.slice(virtualBase.length);
|
|
2705
|
-
}
|
|
2706
|
-
route = route.replace(/^\\/+/, '/') || '/';
|
|
2707
|
-
const modulePath = route === '/' ? '/index' : route;
|
|
2708
|
-
return virtualBase + '/_next/pages' + modulePath + '.js';
|
|
2709
|
-
}
|
|
2710
|
-
|
|
2711
|
-
// Dynamic page loader
|
|
2712
|
-
async function loadPage(pathname) {
|
|
2713
|
-
const modulePath = getPageModulePath(pathname);
|
|
2714
|
-
try {
|
|
2715
|
-
const module = await import(/* @vite-ignore */ modulePath);
|
|
2716
|
-
return module.default;
|
|
2717
|
-
} catch (e) {
|
|
2718
|
-
console.error('[Navigation] Failed to load:', modulePath, e);
|
|
2719
|
-
return null;
|
|
2720
|
-
}
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
// Router component
|
|
2724
|
-
function Router() {
|
|
2725
|
-
const [Page, setPage] = React.useState(null);
|
|
2726
|
-
const [path, setPath] = React.useState(window.location.pathname);
|
|
2727
|
-
|
|
2728
|
-
React.useEffect(() => {
|
|
2729
|
-
loadPage(path).then(C => C && setPage(() => C));
|
|
2730
|
-
}, []);
|
|
2731
|
-
|
|
2732
|
-
React.useEffect(() => {
|
|
2733
|
-
const handleNavigation = async () => {
|
|
2734
|
-
const newPath = window.location.pathname;
|
|
2735
|
-
if (newPath !== path) {
|
|
2736
|
-
setPath(newPath);
|
|
2737
|
-
const C = await loadPage(newPath);
|
|
2738
|
-
if (C) setPage(() => C);
|
|
2739
|
-
}
|
|
2740
|
-
};
|
|
2741
|
-
window.addEventListener('popstate', handleNavigation);
|
|
2742
|
-
return () => window.removeEventListener('popstate', handleNavigation);
|
|
2743
|
-
}, [path]);
|
|
2744
|
-
|
|
2745
|
-
if (!Page) return null;
|
|
2746
|
-
return React.createElement(Page);
|
|
2747
|
-
}
|
|
2748
|
-
|
|
2749
|
-
// Mark that we've initialized (for testing no-reload)
|
|
2750
|
-
window.__NEXT_INITIALIZED__ = Date.now();
|
|
2751
|
-
|
|
2752
|
-
ReactDOM.createRoot(document.getElementById('__next')).render(
|
|
2753
|
-
React.createElement(React.StrictMode, null, React.createElement(Router))
|
|
2754
|
-
);
|
|
2755
|
-
</script>
|
|
2756
|
-
</body>
|
|
2757
|
-
</html>`;
|
|
1857
|
+
return _generatePageHtml(this.htmlContext(), pageFile, pathname);
|
|
2758
1858
|
}
|
|
2759
1859
|
|
|
2760
1860
|
/**
|
|
2761
1861
|
* Serve a basic 404 page
|
|
2762
1862
|
*/
|
|
2763
1863
|
private serve404Page(): ResponseData {
|
|
2764
|
-
|
|
2765
|
-
const html = `<!DOCTYPE html>
|
|
2766
|
-
<html lang="en">
|
|
2767
|
-
<head>
|
|
2768
|
-
<meta charset="UTF-8">
|
|
2769
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2770
|
-
<base href="${virtualPrefix}/">
|
|
2771
|
-
<title>404 - Page Not Found</title>
|
|
2772
|
-
<style>
|
|
2773
|
-
body {
|
|
2774
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2775
|
-
display: flex;
|
|
2776
|
-
flex-direction: column;
|
|
2777
|
-
align-items: center;
|
|
2778
|
-
justify-content: center;
|
|
2779
|
-
min-height: 100vh;
|
|
2780
|
-
margin: 0;
|
|
2781
|
-
background: #fafafa;
|
|
2782
|
-
}
|
|
2783
|
-
h1 { font-size: 48px; margin: 0; }
|
|
2784
|
-
p { color: #666; margin-top: 10px; }
|
|
2785
|
-
a { color: #0070f3; text-decoration: none; }
|
|
2786
|
-
a:hover { text-decoration: underline; }
|
|
2787
|
-
</style>
|
|
2788
|
-
</head>
|
|
2789
|
-
<body>
|
|
2790
|
-
<h1>404</h1>
|
|
2791
|
-
<p>This page could not be found.</p>
|
|
2792
|
-
<p><a href="/">Go back home</a></p>
|
|
2793
|
-
</body>
|
|
2794
|
-
</html>`;
|
|
2795
|
-
|
|
2796
|
-
const buffer = Buffer.from(html);
|
|
2797
|
-
return {
|
|
2798
|
-
statusCode: 404,
|
|
2799
|
-
statusMessage: 'Not Found',
|
|
2800
|
-
headers: {
|
|
2801
|
-
'Content-Type': 'text/html; charset=utf-8',
|
|
2802
|
-
'Content-Length': String(buffer.length),
|
|
2803
|
-
},
|
|
2804
|
-
body: buffer,
|
|
2805
|
-
};
|
|
1864
|
+
return _serve404Page(this.port);
|
|
2806
1865
|
}
|
|
2807
1866
|
|
|
2808
1867
|
/**
|
|
@@ -2874,8 +1933,12 @@ export class NextDevServer extends DevServer {
|
|
|
2874
1933
|
// Use filePath (with extension) for transform so loader is correctly determined
|
|
2875
1934
|
const transformed = await this.transformCode(content, filePath);
|
|
2876
1935
|
|
|
2877
|
-
// Cache the transform result
|
|
1936
|
+
// Cache the transform result (LRU eviction at 500 entries)
|
|
2878
1937
|
this.transformCache.set(filePath, { code: transformed, hash });
|
|
1938
|
+
if (this.transformCache.size > 500) {
|
|
1939
|
+
const firstKey = this.transformCache.keys().next().value;
|
|
1940
|
+
if (firstKey) this.transformCache.delete(firstKey);
|
|
1941
|
+
}
|
|
2879
1942
|
|
|
2880
1943
|
const buffer = Buffer.from(transformed);
|
|
2881
1944
|
return {
|
|
@@ -2910,7 +1973,9 @@ export class NextDevServer extends DevServer {
|
|
|
2910
1973
|
*/
|
|
2911
1974
|
private async transformCode(code: string, filename: string): Promise<string> {
|
|
2912
1975
|
if (!isBrowser) {
|
|
2913
|
-
|
|
1976
|
+
// Even in non-browser mode, strip/transform CSS imports
|
|
1977
|
+
// so CSS module imports get replaced with class name objects
|
|
1978
|
+
return this.stripCssImports(code, filename);
|
|
2914
1979
|
}
|
|
2915
1980
|
|
|
2916
1981
|
await initEsbuild();
|
|
@@ -2922,7 +1987,7 @@ export class NextDevServer extends DevServer {
|
|
|
2922
1987
|
|
|
2923
1988
|
// Remove CSS imports before transformation - they are handled via <link> tags
|
|
2924
1989
|
// CSS imports in ESM would fail with MIME type errors
|
|
2925
|
-
const codeWithoutCssImports = this.stripCssImports(code);
|
|
1990
|
+
const codeWithoutCssImports = this.stripCssImports(code, filename);
|
|
2926
1991
|
|
|
2927
1992
|
// Resolve path aliases (e.g., @/ -> /) before transformation
|
|
2928
1993
|
const codeWithResolvedAliases = this.resolvePathAliases(codeWithoutCssImports, filename);
|
|
@@ -2953,81 +2018,19 @@ export class NextDevServer extends DevServer {
|
|
|
2953
2018
|
return codeWithCdnImports;
|
|
2954
2019
|
}
|
|
2955
2020
|
|
|
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
2021
|
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
|
-
}
|
|
2022
|
+
return _redirectNpmImports(code);
|
|
2023
|
+
}
|
|
3014
2024
|
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
return `${prefix}${esmUrl}${suffix}`;
|
|
3018
|
-
});
|
|
2025
|
+
private stripCssImports(code: string, currentFile?: string): string {
|
|
2026
|
+
return _stripCssImports(code, currentFile, this.getCssModuleContext());
|
|
3019
2027
|
}
|
|
3020
2028
|
|
|
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, '');
|
|
2029
|
+
private getCssModuleContext(): CssModuleContext {
|
|
2030
|
+
return {
|
|
2031
|
+
readFile: (path: string) => this.vfs.readFileSync(path, 'utf-8'),
|
|
2032
|
+
exists: (path: string) => this.exists(path),
|
|
2033
|
+
};
|
|
3031
2034
|
}
|
|
3032
2035
|
|
|
3033
2036
|
/**
|
|
@@ -3062,94 +2065,11 @@ export class NextDevServer extends DevServer {
|
|
|
3062
2065
|
return result.code;
|
|
3063
2066
|
}
|
|
3064
2067
|
|
|
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;
|
|
2068
|
+
return transformEsmToCjsSimple(codeWithResolvedAliases);
|
|
3099
2069
|
}
|
|
3100
2070
|
|
|
3101
|
-
/**
|
|
3102
|
-
* Add React Refresh registration to transformed code
|
|
3103
|
-
*/
|
|
3104
2071
|
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
|
-
`;
|
|
2072
|
+
return _addReactRefresh(code, filename);
|
|
3153
2073
|
}
|
|
3154
2074
|
|
|
3155
2075
|
/**
|