almostnode 0.2.7 → 0.2.9
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 +4 -2
- package/dist/CNAME +1 -0
- package/dist/__sw__.js +80 -84
- package/dist/assets/{runtime-worker-B8_LZkBX.js → runtime-worker-ujGAG2t7.js} +1278 -828
- package/dist/assets/runtime-worker-ujGAG2t7.js.map +1 -0
- package/dist/frameworks/code-transforms.d.ts.map +1 -1
- 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 +6 -6
- 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/index.cjs +3024 -2465
- 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 +3336 -2787
- package/dist/index.mjs.map +1 -1
- package/dist/og-image.png +0 -0
- package/dist/runtime.d.ts +26 -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/types/package-json.d.ts +1 -0
- package/dist/types/package-json.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 +4 -4
- package/src/convex-app-demo-entry.ts +229 -35
- package/src/frameworks/code-transforms.ts +5 -1
- package/src/frameworks/next-config-parser.ts +140 -0
- package/src/frameworks/next-dev-server.ts +76 -1675
- 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/index.ts +2 -0
- package/src/runtime.ts +271 -25
- 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 +312 -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 +95 -2
- package/src/types/package-json.ts +1 -0
- 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,7 @@ 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';
|
|
11
12
|
import {
|
|
12
13
|
redirectNpmImports as _redirectNpmImports,
|
|
13
14
|
stripCssImports as _stripCssImports,
|
|
@@ -15,6 +16,23 @@ import {
|
|
|
15
16
|
transformEsmToCjsSimple,
|
|
16
17
|
type CssModuleContext,
|
|
17
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';
|
|
18
36
|
|
|
19
37
|
// Check if we're in a real browser environment (not jsdom or Node.js)
|
|
20
38
|
const isBrowser = typeof window !== 'undefined' &&
|
|
@@ -91,1061 +109,6 @@ export interface NextDevServerOptions extends DevServerOptions {
|
|
|
91
109
|
basePath?: string;
|
|
92
110
|
}
|
|
93
111
|
|
|
94
|
-
/** Resolved App Router route with page, layouts, and UI convention files */
|
|
95
|
-
interface AppRoute {
|
|
96
|
-
page: string;
|
|
97
|
-
layouts: string[];
|
|
98
|
-
params: Record<string, string | string[]>;
|
|
99
|
-
loading?: string;
|
|
100
|
-
error?: string;
|
|
101
|
-
notFound?: string;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Tailwind CSS CDN script for runtime JIT compilation
|
|
106
|
-
*/
|
|
107
|
-
const TAILWIND_CDN_SCRIPT = `<script src="https://cdn.tailwindcss.com"></script>`;
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* CORS Proxy script - provides proxyFetch function in the iframe
|
|
111
|
-
* Reads proxy URL from localStorage (set by parent window)
|
|
112
|
-
*/
|
|
113
|
-
const CORS_PROXY_SCRIPT = `
|
|
114
|
-
<script>
|
|
115
|
-
// CORS Proxy support for external API calls
|
|
116
|
-
window.__getCorsProxy = function() {
|
|
117
|
-
return localStorage.getItem('__corsProxyUrl') || null;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
window.__setCorsProxy = function(url) {
|
|
121
|
-
if (url) {
|
|
122
|
-
localStorage.setItem('__corsProxyUrl', url);
|
|
123
|
-
} else {
|
|
124
|
-
localStorage.removeItem('__corsProxyUrl');
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
window.__proxyFetch = async function(url, options) {
|
|
129
|
-
const proxyUrl = window.__getCorsProxy();
|
|
130
|
-
if (proxyUrl) {
|
|
131
|
-
const proxiedUrl = proxyUrl + encodeURIComponent(url);
|
|
132
|
-
return fetch(proxiedUrl, options);
|
|
133
|
-
}
|
|
134
|
-
return fetch(url, options);
|
|
135
|
-
};
|
|
136
|
-
</script>
|
|
137
|
-
`;
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* React Refresh preamble - MUST run before React is loaded
|
|
141
|
-
*/
|
|
142
|
-
const REACT_REFRESH_PREAMBLE = `
|
|
143
|
-
<script type="module">
|
|
144
|
-
// Block until React Refresh is loaded and initialized
|
|
145
|
-
const RefreshRuntime = await import('https://esm.sh/react-refresh@0.14.0/runtime').then(m => m.default || m);
|
|
146
|
-
|
|
147
|
-
RefreshRuntime.injectIntoGlobalHook(window);
|
|
148
|
-
window.$RefreshRuntime$ = RefreshRuntime;
|
|
149
|
-
window.$RefreshRegCount$ = 0;
|
|
150
|
-
|
|
151
|
-
window.$RefreshReg$ = (type, id) => {
|
|
152
|
-
window.$RefreshRegCount$++;
|
|
153
|
-
RefreshRuntime.register(type, id);
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
window.$RefreshSig$ = () => (type) => type;
|
|
157
|
-
|
|
158
|
-
console.log('[HMR] React Refresh initialized');
|
|
159
|
-
</script>
|
|
160
|
-
`;
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* HMR client script for Next.js
|
|
164
|
-
*/
|
|
165
|
-
const HMR_CLIENT_SCRIPT = `
|
|
166
|
-
<script type="module">
|
|
167
|
-
(function() {
|
|
168
|
-
const hotModules = new Map();
|
|
169
|
-
const pendingUpdates = new Map();
|
|
170
|
-
|
|
171
|
-
window.__vite_hot_context__ = function createHotContext(ownerPath) {
|
|
172
|
-
if (hotModules.has(ownerPath)) {
|
|
173
|
-
return hotModules.get(ownerPath);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const hot = {
|
|
177
|
-
data: {},
|
|
178
|
-
accept(callback) {
|
|
179
|
-
hot._acceptCallback = callback;
|
|
180
|
-
},
|
|
181
|
-
dispose(callback) {
|
|
182
|
-
hot._disposeCallback = callback;
|
|
183
|
-
},
|
|
184
|
-
invalidate() {
|
|
185
|
-
location.reload();
|
|
186
|
-
},
|
|
187
|
-
prune(callback) {
|
|
188
|
-
hot._pruneCallback = callback;
|
|
189
|
-
},
|
|
190
|
-
on(event, cb) {},
|
|
191
|
-
off(event, cb) {},
|
|
192
|
-
send(event, data) {},
|
|
193
|
-
_acceptCallback: null,
|
|
194
|
-
_disposeCallback: null,
|
|
195
|
-
_pruneCallback: null,
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
hotModules.set(ownerPath, hot);
|
|
199
|
-
return hot;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
// Listen for HMR updates via postMessage (works with sandboxed iframes)
|
|
203
|
-
window.addEventListener('message', async (event) => {
|
|
204
|
-
// Filter for HMR messages only
|
|
205
|
-
if (!event.data || event.data.channel !== 'next-hmr') return;
|
|
206
|
-
const { type, path, timestamp } = event.data;
|
|
207
|
-
|
|
208
|
-
if (type === 'update') {
|
|
209
|
-
console.log('[HMR] Update:', path);
|
|
210
|
-
|
|
211
|
-
if (path.endsWith('.css')) {
|
|
212
|
-
const links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
213
|
-
links.forEach(link => {
|
|
214
|
-
const href = link.getAttribute('href');
|
|
215
|
-
if (href && href.includes(path.replace(/^\\//, ''))) {
|
|
216
|
-
link.href = href.split('?')[0] + '?t=' + timestamp;
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const styles = document.querySelectorAll('style[data-next-dev-id]');
|
|
221
|
-
styles.forEach(style => {
|
|
222
|
-
const id = style.getAttribute('data-next-dev-id');
|
|
223
|
-
if (id && id.includes(path.replace(/^\\//, ''))) {
|
|
224
|
-
import(path + '?t=' + timestamp).catch(() => {});
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
} else if (path.match(/\\.(jsx?|tsx?)$/)) {
|
|
228
|
-
await handleJSUpdate(path, timestamp);
|
|
229
|
-
}
|
|
230
|
-
} else if (type === 'full-reload') {
|
|
231
|
-
console.log('[HMR] Full reload');
|
|
232
|
-
location.reload();
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
async function handleJSUpdate(path, timestamp) {
|
|
237
|
-
const normalizedPath = path.startsWith('/') ? path : '/' + path;
|
|
238
|
-
const hot = hotModules.get(normalizedPath);
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
if (hot && hot._disposeCallback) {
|
|
242
|
-
hot._disposeCallback(hot.data);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (window.$RefreshRuntime$) {
|
|
246
|
-
pendingUpdates.set(normalizedPath, timestamp);
|
|
247
|
-
|
|
248
|
-
if (pendingUpdates.size === 1) {
|
|
249
|
-
setTimeout(async () => {
|
|
250
|
-
try {
|
|
251
|
-
for (const [modulePath, ts] of pendingUpdates) {
|
|
252
|
-
const moduleUrl = '.' + modulePath + '?t=' + ts;
|
|
253
|
-
await import(moduleUrl);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
window.$RefreshRuntime$.performReactRefresh();
|
|
257
|
-
console.log('[HMR] Updated', pendingUpdates.size, 'module(s)');
|
|
258
|
-
|
|
259
|
-
pendingUpdates.clear();
|
|
260
|
-
} catch (error) {
|
|
261
|
-
console.error('[HMR] Failed to apply update:', error);
|
|
262
|
-
pendingUpdates.clear();
|
|
263
|
-
location.reload();
|
|
264
|
-
}
|
|
265
|
-
}, 30);
|
|
266
|
-
}
|
|
267
|
-
} else {
|
|
268
|
-
console.log('[HMR] React Refresh not available, reloading page');
|
|
269
|
-
location.reload();
|
|
270
|
-
}
|
|
271
|
-
} catch (error) {
|
|
272
|
-
console.error('[HMR] Update failed:', error);
|
|
273
|
-
location.reload();
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
console.log('[HMR] Next.js client ready');
|
|
278
|
-
})();
|
|
279
|
-
</script>
|
|
280
|
-
`;
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Next.js Link shim code
|
|
284
|
-
*/
|
|
285
|
-
const NEXT_LINK_SHIM = `
|
|
286
|
-
import React from 'react';
|
|
287
|
-
|
|
288
|
-
const getVirtualBasePath = () => {
|
|
289
|
-
const match = window.location.pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
|
|
290
|
-
if (!match) return '';
|
|
291
|
-
return match[0].endsWith('/') ? match[0] : match[0] + '/';
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
const getBasePath = () => window.__NEXT_BASE_PATH__ || '';
|
|
295
|
-
|
|
296
|
-
const applyVirtualBase = (url) => {
|
|
297
|
-
if (typeof url !== 'string') return url;
|
|
298
|
-
if (!url || url.startsWith('#') || url.startsWith('?')) return url;
|
|
299
|
-
if (/^(https?:)?\\/\\//.test(url)) return url;
|
|
300
|
-
|
|
301
|
-
// Apply basePath first
|
|
302
|
-
const bp = getBasePath();
|
|
303
|
-
if (bp && url.startsWith('/') && !url.startsWith(bp + '/') && url !== bp) {
|
|
304
|
-
url = bp + url;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const base = getVirtualBasePath();
|
|
308
|
-
if (!base) return url;
|
|
309
|
-
if (url.startsWith(base)) return url;
|
|
310
|
-
if (url.startsWith('/')) return base + url.slice(1);
|
|
311
|
-
return base + url;
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
export default function Link({ href, children, ...props }) {
|
|
315
|
-
const handleClick = (e) => {
|
|
316
|
-
console.log('[Link] Click handler called, href:', href);
|
|
317
|
-
|
|
318
|
-
if (props.onClick) {
|
|
319
|
-
props.onClick(e);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Allow cmd/ctrl click to open in new tab
|
|
323
|
-
if (e.metaKey || e.ctrlKey) {
|
|
324
|
-
console.log('[Link] Meta/Ctrl key pressed, allowing default behavior');
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (typeof href !== 'string' || !href || href.startsWith('#') || href.startsWith('?')) {
|
|
329
|
-
console.log('[Link] Skipping navigation for href:', href);
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (/^(https?:)?\\/\\//.test(href)) {
|
|
334
|
-
console.log('[Link] External URL, allowing default behavior:', href);
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
e.preventDefault();
|
|
339
|
-
const resolvedHref = applyVirtualBase(href);
|
|
340
|
-
console.log('[Link] Navigating to:', resolvedHref);
|
|
341
|
-
window.history.pushState({}, '', resolvedHref);
|
|
342
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
return React.createElement('a', { href, onClick: handleClick, ...props }, children);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
export { Link };
|
|
349
|
-
`;
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Next.js Router shim code
|
|
353
|
-
*/
|
|
354
|
-
const NEXT_ROUTER_SHIM = `
|
|
355
|
-
import React, { useState, useEffect, createContext, useContext } from 'react';
|
|
356
|
-
|
|
357
|
-
const RouterContext = createContext(null);
|
|
358
|
-
|
|
359
|
-
const getVirtualBasePath = () => {
|
|
360
|
-
const match = window.location.pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
|
|
361
|
-
if (!match) return '';
|
|
362
|
-
return match[0].endsWith('/') ? match[0] : match[0] + '/';
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
const applyVirtualBase = (url) => {
|
|
366
|
-
if (typeof url !== 'string') return url;
|
|
367
|
-
if (!url || url.startsWith('#') || url.startsWith('?')) return url;
|
|
368
|
-
if (/^(https?:)?\\/\\//.test(url)) return url;
|
|
369
|
-
|
|
370
|
-
const base = getVirtualBasePath();
|
|
371
|
-
if (!base) return url;
|
|
372
|
-
if (url.startsWith(base)) return url;
|
|
373
|
-
if (url.startsWith('/')) return base + url.slice(1);
|
|
374
|
-
return base + url;
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
const stripVirtualBase = (pathname) => {
|
|
378
|
-
const match = pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
|
|
379
|
-
if (!match) return pathname;
|
|
380
|
-
return '/' + pathname.slice(match[0].length);
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
export function useRouter() {
|
|
384
|
-
const [pathname, setPathname] = useState(
|
|
385
|
-
typeof window !== 'undefined' ? stripVirtualBase(window.location.pathname) : '/'
|
|
386
|
-
);
|
|
387
|
-
const [query, setQuery] = useState({});
|
|
388
|
-
|
|
389
|
-
useEffect(() => {
|
|
390
|
-
const updateRoute = () => {
|
|
391
|
-
setPathname(stripVirtualBase(window.location.pathname));
|
|
392
|
-
setQuery(Object.fromEntries(new URLSearchParams(window.location.search)));
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
window.addEventListener('popstate', updateRoute);
|
|
396
|
-
updateRoute();
|
|
397
|
-
|
|
398
|
-
return () => window.removeEventListener('popstate', updateRoute);
|
|
399
|
-
}, []);
|
|
400
|
-
|
|
401
|
-
return {
|
|
402
|
-
pathname,
|
|
403
|
-
query,
|
|
404
|
-
asPath: pathname + window.location.search,
|
|
405
|
-
push: (url, as, options) => {
|
|
406
|
-
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
407
|
-
window.location.href = url;
|
|
408
|
-
return Promise.resolve(true);
|
|
409
|
-
}
|
|
410
|
-
const resolvedUrl = applyVirtualBase(url);
|
|
411
|
-
window.history.pushState({}, '', resolvedUrl);
|
|
412
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
413
|
-
return Promise.resolve(true);
|
|
414
|
-
},
|
|
415
|
-
replace: (url, as, options) => {
|
|
416
|
-
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
417
|
-
window.location.href = url;
|
|
418
|
-
return Promise.resolve(true);
|
|
419
|
-
}
|
|
420
|
-
const resolvedUrl = applyVirtualBase(url);
|
|
421
|
-
window.history.replaceState({}, '', resolvedUrl);
|
|
422
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
423
|
-
return Promise.resolve(true);
|
|
424
|
-
},
|
|
425
|
-
prefetch: () => Promise.resolve(),
|
|
426
|
-
back: () => window.history.back(),
|
|
427
|
-
forward: () => window.history.forward(),
|
|
428
|
-
reload: () => window.location.reload(),
|
|
429
|
-
events: {
|
|
430
|
-
on: () => {},
|
|
431
|
-
off: () => {},
|
|
432
|
-
emit: () => {},
|
|
433
|
-
},
|
|
434
|
-
isFallback: false,
|
|
435
|
-
isReady: true,
|
|
436
|
-
isPreview: false,
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
export const Router = {
|
|
441
|
-
events: {
|
|
442
|
-
on: () => {},
|
|
443
|
-
off: () => {},
|
|
444
|
-
emit: () => {},
|
|
445
|
-
},
|
|
446
|
-
push: (url) => {
|
|
447
|
-
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
448
|
-
window.location.href = url;
|
|
449
|
-
return Promise.resolve(true);
|
|
450
|
-
}
|
|
451
|
-
const resolvedUrl = applyVirtualBase(url);
|
|
452
|
-
window.history.pushState({}, '', resolvedUrl);
|
|
453
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
454
|
-
return Promise.resolve(true);
|
|
455
|
-
},
|
|
456
|
-
replace: (url) => {
|
|
457
|
-
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
458
|
-
window.location.href = url;
|
|
459
|
-
return Promise.resolve(true);
|
|
460
|
-
}
|
|
461
|
-
const resolvedUrl = applyVirtualBase(url);
|
|
462
|
-
window.history.replaceState({}, '', resolvedUrl);
|
|
463
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
464
|
-
return Promise.resolve(true);
|
|
465
|
-
},
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
export default { useRouter, Router };
|
|
469
|
-
`;
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Next.js Navigation shim code (App Router)
|
|
473
|
-
*
|
|
474
|
-
* This shim provides App Router-specific navigation hooks from 'next/navigation'.
|
|
475
|
-
* These are DIFFERENT from the Pages Router hooks in 'next/router':
|
|
476
|
-
*
|
|
477
|
-
* Pages Router (next/router):
|
|
478
|
-
* - useRouter() returns { pathname, query, push, replace, events, ... }
|
|
479
|
-
* - Has router.events for route change subscriptions
|
|
480
|
-
* - query object contains URL params
|
|
481
|
-
*
|
|
482
|
-
* App Router (next/navigation):
|
|
483
|
-
* - useRouter() returns { push, replace, back, forward, refresh, prefetch }
|
|
484
|
-
* - usePathname() for current path
|
|
485
|
-
* - useSearchParams() for URL search params
|
|
486
|
-
* - useParams() for dynamic route segments
|
|
487
|
-
* - No events - use useEffect with pathname/searchParams instead
|
|
488
|
-
*
|
|
489
|
-
* @see https://nextjs.org/docs/app/api-reference/functions/use-router
|
|
490
|
-
*/
|
|
491
|
-
const NEXT_NAVIGATION_SHIM = `
|
|
492
|
-
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
493
|
-
|
|
494
|
-
const getVirtualBasePath = () => {
|
|
495
|
-
const match = window.location.pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
|
|
496
|
-
if (!match) return '';
|
|
497
|
-
return match[0].endsWith('/') ? match[0] : match[0] + '/';
|
|
498
|
-
};
|
|
499
|
-
|
|
500
|
-
const applyVirtualBase = (url) => {
|
|
501
|
-
if (typeof url !== 'string') return url;
|
|
502
|
-
if (!url || url.startsWith('#') || url.startsWith('?')) return url;
|
|
503
|
-
if (/^(https?:)?\\/\\//.test(url)) return url;
|
|
504
|
-
|
|
505
|
-
const base = getVirtualBasePath();
|
|
506
|
-
if (!base) return url;
|
|
507
|
-
if (url.startsWith(base)) return url;
|
|
508
|
-
if (url.startsWith('/')) return base + url.slice(1);
|
|
509
|
-
return base + url;
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
const stripVirtualBase = (pathname) => {
|
|
513
|
-
const match = pathname.match(/^\\/__virtual__\\/\\d+(?:\\/|$)/);
|
|
514
|
-
if (!match) return pathname;
|
|
515
|
-
return '/' + pathname.slice(match[0].length);
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* App Router's useRouter hook
|
|
520
|
-
* Returns navigation methods only (no pathname, no query)
|
|
521
|
-
* Use usePathname() and useSearchParams() for URL info
|
|
522
|
-
*/
|
|
523
|
-
export function useRouter() {
|
|
524
|
-
const push = useCallback((url, options) => {
|
|
525
|
-
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
526
|
-
window.location.href = url;
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
const resolvedUrl = applyVirtualBase(url);
|
|
530
|
-
window.history.pushState({}, '', resolvedUrl);
|
|
531
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
532
|
-
}, []);
|
|
533
|
-
|
|
534
|
-
const replace = useCallback((url, options) => {
|
|
535
|
-
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
536
|
-
window.location.href = url;
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
const resolvedUrl = applyVirtualBase(url);
|
|
540
|
-
window.history.replaceState({}, '', resolvedUrl);
|
|
541
|
-
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
542
|
-
}, []);
|
|
543
|
-
|
|
544
|
-
const back = useCallback(() => window.history.back(), []);
|
|
545
|
-
const forward = useCallback(() => window.history.forward(), []);
|
|
546
|
-
const refresh = useCallback(() => window.location.reload(), []);
|
|
547
|
-
const prefetch = useCallback(() => Promise.resolve(), []);
|
|
548
|
-
|
|
549
|
-
return useMemo(() => ({
|
|
550
|
-
push,
|
|
551
|
-
replace,
|
|
552
|
-
back,
|
|
553
|
-
forward,
|
|
554
|
-
refresh,
|
|
555
|
-
prefetch,
|
|
556
|
-
}), [push, replace, back, forward, refresh, prefetch]);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* usePathname - Returns the current URL pathname
|
|
561
|
-
* Reactively updates when navigation occurs
|
|
562
|
-
* @example const pathname = usePathname(); // '/dashboard/settings'
|
|
563
|
-
*/
|
|
564
|
-
export function usePathname() {
|
|
565
|
-
const [pathname, setPathname] = useState(
|
|
566
|
-
typeof window !== 'undefined' ? stripVirtualBase(window.location.pathname) : '/'
|
|
567
|
-
);
|
|
568
|
-
|
|
569
|
-
useEffect(() => {
|
|
570
|
-
const handler = () => setPathname(stripVirtualBase(window.location.pathname));
|
|
571
|
-
window.addEventListener('popstate', handler);
|
|
572
|
-
return () => window.removeEventListener('popstate', handler);
|
|
573
|
-
}, []);
|
|
574
|
-
|
|
575
|
-
return pathname;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* useSearchParams - Returns the current URL search parameters
|
|
580
|
-
* @example const searchParams = useSearchParams();
|
|
581
|
-
* const query = searchParams.get('q'); // '?q=hello' -> 'hello'
|
|
582
|
-
*/
|
|
583
|
-
export function useSearchParams() {
|
|
584
|
-
const [searchParams, setSearchParams] = useState(() => {
|
|
585
|
-
if (typeof window === 'undefined') return new URLSearchParams();
|
|
586
|
-
return new URLSearchParams(window.location.search);
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
useEffect(() => {
|
|
590
|
-
const handler = () => {
|
|
591
|
-
setSearchParams(new URLSearchParams(window.location.search));
|
|
592
|
-
};
|
|
593
|
-
window.addEventListener('popstate', handler);
|
|
594
|
-
return () => window.removeEventListener('popstate', handler);
|
|
595
|
-
}, []);
|
|
596
|
-
|
|
597
|
-
return searchParams;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/**
|
|
601
|
-
* useParams - Returns dynamic route parameters
|
|
602
|
-
* For route /users/[id]/page.jsx with URL /users/123:
|
|
603
|
-
* @example const { id } = useParams(); // { id: '123' }
|
|
604
|
-
*
|
|
605
|
-
* Fetches params from the server's route-info endpoint for dynamic routes.
|
|
606
|
-
*/
|
|
607
|
-
export function useParams() {
|
|
608
|
-
const [params, setParams] = useState(() => {
|
|
609
|
-
// Check if initial params were embedded by the server
|
|
610
|
-
if (typeof window !== 'undefined' && window.__NEXT_ROUTE_PARAMS__) {
|
|
611
|
-
return window.__NEXT_ROUTE_PARAMS__;
|
|
612
|
-
}
|
|
613
|
-
return {};
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
useEffect(() => {
|
|
617
|
-
let cancelled = false;
|
|
618
|
-
|
|
619
|
-
const fetchParams = async () => {
|
|
620
|
-
const pathname = stripVirtualBase(window.location.pathname);
|
|
621
|
-
const base = getVirtualBasePath();
|
|
622
|
-
const baseUrl = base ? base.replace(/\\/$/, '') : '';
|
|
623
|
-
|
|
624
|
-
try {
|
|
625
|
-
const response = await fetch(baseUrl + '/_next/route-info?pathname=' + encodeURIComponent(pathname));
|
|
626
|
-
const info = await response.json();
|
|
627
|
-
if (!cancelled && info.params) {
|
|
628
|
-
setParams(info.params);
|
|
629
|
-
}
|
|
630
|
-
} catch (e) {
|
|
631
|
-
// Silently fail - static routes won't have params
|
|
632
|
-
}
|
|
633
|
-
};
|
|
634
|
-
|
|
635
|
-
fetchParams();
|
|
636
|
-
|
|
637
|
-
const handler = () => fetchParams();
|
|
638
|
-
window.addEventListener('popstate', handler);
|
|
639
|
-
return () => {
|
|
640
|
-
cancelled = true;
|
|
641
|
-
window.removeEventListener('popstate', handler);
|
|
642
|
-
};
|
|
643
|
-
}, []);
|
|
644
|
-
|
|
645
|
-
return params;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* useSelectedLayoutSegment - Returns the active child segment one level below
|
|
650
|
-
* Useful for styling active nav items in layouts
|
|
651
|
-
* @example For /dashboard/settings, returns 'settings' in dashboard layout
|
|
652
|
-
*/
|
|
653
|
-
export function useSelectedLayoutSegment() {
|
|
654
|
-
const pathname = usePathname();
|
|
655
|
-
const segments = pathname.split('/').filter(Boolean);
|
|
656
|
-
return segments[0] || null;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* useSelectedLayoutSegments - Returns all active child segments
|
|
661
|
-
* @example For /dashboard/settings/profile, returns ['dashboard', 'settings', 'profile']
|
|
662
|
-
*/
|
|
663
|
-
export function useSelectedLayoutSegments() {
|
|
664
|
-
const pathname = usePathname();
|
|
665
|
-
return pathname.split('/').filter(Boolean);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
* redirect - Programmatic redirect (typically used in Server Components)
|
|
670
|
-
* In this browser implementation, performs immediate navigation
|
|
671
|
-
*/
|
|
672
|
-
export function redirect(url) {
|
|
673
|
-
if (typeof url === 'string' && /^(https?:)?\\/\\//.test(url)) {
|
|
674
|
-
window.location.href = url;
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
window.location.href = applyVirtualBase(url);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
* notFound - Trigger the not-found UI
|
|
682
|
-
* In this browser implementation, throws an error
|
|
683
|
-
*/
|
|
684
|
-
export function notFound() {
|
|
685
|
-
throw new Error('NEXT_NOT_FOUND');
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// Re-export Link for convenience (can import from next/navigation or next/link)
|
|
689
|
-
export { default as Link } from 'next/link';
|
|
690
|
-
`;
|
|
691
|
-
|
|
692
|
-
/**
|
|
693
|
-
* Next.js Head shim code
|
|
694
|
-
*/
|
|
695
|
-
const NEXT_HEAD_SHIM = `
|
|
696
|
-
import React, { useEffect } from 'react';
|
|
697
|
-
|
|
698
|
-
export default function Head({ children }) {
|
|
699
|
-
useEffect(() => {
|
|
700
|
-
// Process children and update document.head
|
|
701
|
-
React.Children.forEach(children, (child) => {
|
|
702
|
-
if (!React.isValidElement(child)) return;
|
|
703
|
-
|
|
704
|
-
const { type, props } = child;
|
|
705
|
-
|
|
706
|
-
if (type === 'title' && props.children) {
|
|
707
|
-
document.title = Array.isArray(props.children)
|
|
708
|
-
? props.children.join('')
|
|
709
|
-
: props.children;
|
|
710
|
-
} else if (type === 'meta') {
|
|
711
|
-
const existingMeta = props.name
|
|
712
|
-
? document.querySelector(\`meta[name="\${props.name}"]\`)
|
|
713
|
-
: props.property
|
|
714
|
-
? document.querySelector(\`meta[property="\${props.property}"]\`)
|
|
715
|
-
: null;
|
|
716
|
-
|
|
717
|
-
if (existingMeta) {
|
|
718
|
-
Object.keys(props).forEach(key => {
|
|
719
|
-
existingMeta.setAttribute(key, props[key]);
|
|
720
|
-
});
|
|
721
|
-
} else {
|
|
722
|
-
const meta = document.createElement('meta');
|
|
723
|
-
Object.keys(props).forEach(key => {
|
|
724
|
-
meta.setAttribute(key, props[key]);
|
|
725
|
-
});
|
|
726
|
-
document.head.appendChild(meta);
|
|
727
|
-
}
|
|
728
|
-
} else if (type === 'link') {
|
|
729
|
-
const link = document.createElement('link');
|
|
730
|
-
Object.keys(props).forEach(key => {
|
|
731
|
-
link.setAttribute(key, props[key]);
|
|
732
|
-
});
|
|
733
|
-
document.head.appendChild(link);
|
|
734
|
-
}
|
|
735
|
-
});
|
|
736
|
-
}, [children]);
|
|
737
|
-
|
|
738
|
-
return null;
|
|
739
|
-
}
|
|
740
|
-
`;
|
|
741
|
-
|
|
742
|
-
/**
|
|
743
|
-
* Next.js Image shim code
|
|
744
|
-
* Provides a simple img-based implementation of next/image
|
|
745
|
-
*/
|
|
746
|
-
const NEXT_IMAGE_SHIM = `
|
|
747
|
-
import React from 'react';
|
|
748
|
-
|
|
749
|
-
function Image({
|
|
750
|
-
src,
|
|
751
|
-
alt = '',
|
|
752
|
-
width,
|
|
753
|
-
height,
|
|
754
|
-
fill,
|
|
755
|
-
loader,
|
|
756
|
-
quality = 75,
|
|
757
|
-
priority,
|
|
758
|
-
loading,
|
|
759
|
-
placeholder,
|
|
760
|
-
blurDataURL,
|
|
761
|
-
unoptimized,
|
|
762
|
-
onLoad,
|
|
763
|
-
onError,
|
|
764
|
-
style,
|
|
765
|
-
className,
|
|
766
|
-
sizes,
|
|
767
|
-
...rest
|
|
768
|
-
}) {
|
|
769
|
-
// Handle src - could be string or StaticImageData object
|
|
770
|
-
const imageSrc = typeof src === 'object' ? src.src : src;
|
|
771
|
-
|
|
772
|
-
// Build style object
|
|
773
|
-
const imgStyle = { ...style };
|
|
774
|
-
if (fill) {
|
|
775
|
-
imgStyle.position = 'absolute';
|
|
776
|
-
imgStyle.width = '100%';
|
|
777
|
-
imgStyle.height = '100%';
|
|
778
|
-
imgStyle.objectFit = imgStyle.objectFit || 'cover';
|
|
779
|
-
imgStyle.inset = '0';
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
return React.createElement('img', {
|
|
783
|
-
src: imageSrc,
|
|
784
|
-
alt,
|
|
785
|
-
width: fill ? undefined : width,
|
|
786
|
-
height: fill ? undefined : height,
|
|
787
|
-
loading: priority ? 'eager' : (loading || 'lazy'),
|
|
788
|
-
decoding: 'async',
|
|
789
|
-
style: imgStyle,
|
|
790
|
-
className,
|
|
791
|
-
onLoad,
|
|
792
|
-
onError,
|
|
793
|
-
...rest
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
export default Image;
|
|
798
|
-
export { Image };
|
|
799
|
-
`;
|
|
800
|
-
|
|
801
|
-
/**
|
|
802
|
-
* next/dynamic shim - Dynamic imports with loading states
|
|
803
|
-
*/
|
|
804
|
-
const NEXT_DYNAMIC_SHIM = `
|
|
805
|
-
import React from 'react';
|
|
806
|
-
|
|
807
|
-
function dynamic(importFn, options = {}) {
|
|
808
|
-
const {
|
|
809
|
-
loading: LoadingComponent,
|
|
810
|
-
ssr = true,
|
|
811
|
-
} = options;
|
|
812
|
-
|
|
813
|
-
// Create a lazy component
|
|
814
|
-
const LazyComponent = React.lazy(importFn);
|
|
815
|
-
|
|
816
|
-
// Wrapper component that handles loading state
|
|
817
|
-
function DynamicComponent(props) {
|
|
818
|
-
const fallback = LoadingComponent
|
|
819
|
-
? React.createElement(LoadingComponent, { isLoading: true })
|
|
820
|
-
: null;
|
|
821
|
-
|
|
822
|
-
return React.createElement(
|
|
823
|
-
React.Suspense,
|
|
824
|
-
{ fallback },
|
|
825
|
-
React.createElement(LazyComponent, props)
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
return DynamicComponent;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
export default dynamic;
|
|
833
|
-
export { dynamic };
|
|
834
|
-
`;
|
|
835
|
-
|
|
836
|
-
/**
|
|
837
|
-
* next/script shim - Loads external scripts
|
|
838
|
-
*/
|
|
839
|
-
const NEXT_SCRIPT_SHIM = `
|
|
840
|
-
import React from 'react';
|
|
841
|
-
|
|
842
|
-
function Script({
|
|
843
|
-
src,
|
|
844
|
-
strategy = 'afterInteractive',
|
|
845
|
-
onLoad,
|
|
846
|
-
onReady,
|
|
847
|
-
onError,
|
|
848
|
-
children,
|
|
849
|
-
dangerouslySetInnerHTML,
|
|
850
|
-
...rest
|
|
851
|
-
}) {
|
|
852
|
-
React.useEffect(function() {
|
|
853
|
-
if (!src && !children && !dangerouslySetInnerHTML) return;
|
|
854
|
-
|
|
855
|
-
var script = document.createElement('script');
|
|
856
|
-
|
|
857
|
-
if (src) {
|
|
858
|
-
script.src = src;
|
|
859
|
-
script.async = strategy !== 'beforeInteractive';
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
Object.keys(rest).forEach(function(key) {
|
|
863
|
-
script.setAttribute(key, rest[key]);
|
|
864
|
-
});
|
|
865
|
-
|
|
866
|
-
if (children) {
|
|
867
|
-
script.textContent = children;
|
|
868
|
-
} else if (dangerouslySetInnerHTML && dangerouslySetInnerHTML.__html) {
|
|
869
|
-
script.textContent = dangerouslySetInnerHTML.__html;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
script.onload = function() {
|
|
873
|
-
if (onLoad) onLoad();
|
|
874
|
-
if (onReady) onReady();
|
|
875
|
-
};
|
|
876
|
-
script.onerror = onError;
|
|
877
|
-
|
|
878
|
-
document.head.appendChild(script);
|
|
879
|
-
|
|
880
|
-
return function() {
|
|
881
|
-
if (script.parentNode) {
|
|
882
|
-
script.parentNode.removeChild(script);
|
|
883
|
-
}
|
|
884
|
-
};
|
|
885
|
-
}, [src]);
|
|
886
|
-
|
|
887
|
-
return null;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
export default Script;
|
|
891
|
-
export { Script };
|
|
892
|
-
`;
|
|
893
|
-
|
|
894
|
-
/**
|
|
895
|
-
* next/font/google shim - Loads Google Fonts via CDN
|
|
896
|
-
* Uses a Proxy to dynamically handle ANY Google Font without hardcoding
|
|
897
|
-
*/
|
|
898
|
-
const NEXT_FONT_GOOGLE_SHIM = `
|
|
899
|
-
// Track loaded fonts to avoid duplicate style injections
|
|
900
|
-
const loadedFonts = new Set();
|
|
901
|
-
|
|
902
|
-
/**
|
|
903
|
-
* Convert font function name to Google Fonts family name
|
|
904
|
-
* Examples:
|
|
905
|
-
* DM_Sans -> DM Sans
|
|
906
|
-
* Open_Sans -> Open Sans
|
|
907
|
-
* Fraunces -> Fraunces
|
|
908
|
-
*/
|
|
909
|
-
function toFontFamily(fontName) {
|
|
910
|
-
return fontName.replace(/_/g, ' ');
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
/**
|
|
914
|
-
* Inject font CSS into document
|
|
915
|
-
* - Adds preconnect links for faster font loading
|
|
916
|
-
* - Loads the font from Google Fonts CDN
|
|
917
|
-
* - Creates a CSS class that sets the CSS variable
|
|
918
|
-
*/
|
|
919
|
-
function injectFontCSS(fontFamily, variableName, weight, style) {
|
|
920
|
-
const fontKey = fontFamily + '-' + (variableName || 'default');
|
|
921
|
-
if (loadedFonts.has(fontKey)) {
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
924
|
-
loadedFonts.add(fontKey);
|
|
925
|
-
|
|
926
|
-
if (typeof document === 'undefined') {
|
|
927
|
-
return;
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// Add preconnect links for faster loading (only once)
|
|
931
|
-
if (!document.querySelector('link[href="https://fonts.googleapis.com"]')) {
|
|
932
|
-
const preconnect1 = document.createElement('link');
|
|
933
|
-
preconnect1.rel = 'preconnect';
|
|
934
|
-
preconnect1.href = 'https://fonts.googleapis.com';
|
|
935
|
-
document.head.appendChild(preconnect1);
|
|
936
|
-
|
|
937
|
-
const preconnect2 = document.createElement('link');
|
|
938
|
-
preconnect2.rel = 'preconnect';
|
|
939
|
-
preconnect2.href = 'https://fonts.gstatic.com';
|
|
940
|
-
preconnect2.crossOrigin = 'anonymous';
|
|
941
|
-
document.head.appendChild(preconnect2);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
// Build Google Fonts URL
|
|
945
|
-
const escapedFamily = fontFamily.replace(/ /g, '+');
|
|
946
|
-
|
|
947
|
-
// Build axis list based on options
|
|
948
|
-
let axisList = '';
|
|
949
|
-
const axes = [];
|
|
950
|
-
|
|
951
|
-
// Handle italic style
|
|
952
|
-
if (style === 'italic') {
|
|
953
|
-
axes.push('ital');
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
// Handle weight - use specific weight or variable range
|
|
957
|
-
if (weight && weight !== '400' && !Array.isArray(weight)) {
|
|
958
|
-
// Specific weight requested
|
|
959
|
-
axes.push('wght');
|
|
960
|
-
if (style === 'italic') {
|
|
961
|
-
axisList = ':ital,wght@1,' + weight;
|
|
962
|
-
} else {
|
|
963
|
-
axisList = ':wght@' + weight;
|
|
964
|
-
}
|
|
965
|
-
} else if (Array.isArray(weight)) {
|
|
966
|
-
// Multiple weights
|
|
967
|
-
axes.push('wght');
|
|
968
|
-
axisList = ':wght@' + weight.join(';');
|
|
969
|
-
} else {
|
|
970
|
-
// Default: request common weights for flexibility
|
|
971
|
-
axisList = ':wght@400;500;600;700';
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
const fontUrl = 'https://fonts.googleapis.com/css2?family=' +
|
|
975
|
-
escapedFamily + axisList + '&display=swap';
|
|
976
|
-
|
|
977
|
-
// Add link element for Google Fonts (if not already present)
|
|
978
|
-
if (!document.querySelector('link[href*="family=' + escapedFamily + '"]')) {
|
|
979
|
-
const link = document.createElement('link');
|
|
980
|
-
link.rel = 'stylesheet';
|
|
981
|
-
link.href = fontUrl;
|
|
982
|
-
document.head.appendChild(link);
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
// Create style element for CSS variable at :root level (globally available)
|
|
986
|
-
// This makes the variable work without needing to apply the class to body
|
|
987
|
-
if (variableName) {
|
|
988
|
-
const styleEl = document.createElement('style');
|
|
989
|
-
styleEl.setAttribute('data-font-var', variableName);
|
|
990
|
-
styleEl.textContent = ':root { ' + variableName + ': "' + fontFamily + '", ' + (fontFamily.includes('Serif') ? 'serif' : 'sans-serif') + '; }';
|
|
991
|
-
document.head.appendChild(styleEl);
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
/**
|
|
996
|
-
* Create a font loader function for a specific font
|
|
997
|
-
*/
|
|
998
|
-
function createFontLoader(fontName) {
|
|
999
|
-
const fontFamily = toFontFamily(fontName);
|
|
1000
|
-
|
|
1001
|
-
return function(options = {}) {
|
|
1002
|
-
const {
|
|
1003
|
-
weight,
|
|
1004
|
-
style = 'normal',
|
|
1005
|
-
subsets = ['latin'],
|
|
1006
|
-
variable,
|
|
1007
|
-
display = 'swap',
|
|
1008
|
-
preload = true,
|
|
1009
|
-
fallback = ['sans-serif'],
|
|
1010
|
-
adjustFontFallback = true
|
|
1011
|
-
} = options;
|
|
1012
|
-
|
|
1013
|
-
// Inject the font CSS
|
|
1014
|
-
injectFontCSS(fontFamily, variable, weight, style);
|
|
1015
|
-
|
|
1016
|
-
// Generate class name from variable (--font-inter -> __font-inter)
|
|
1017
|
-
const className = variable
|
|
1018
|
-
? variable.replace('--', '__')
|
|
1019
|
-
: '__font-' + fontName.toLowerCase().replace(/_/g, '-');
|
|
1020
|
-
|
|
1021
|
-
return {
|
|
1022
|
-
className,
|
|
1023
|
-
variable: className,
|
|
1024
|
-
style: {
|
|
1025
|
-
fontFamily: '"' + fontFamily + '", ' + fallback.join(', ')
|
|
1026
|
-
}
|
|
1027
|
-
};
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
/**
|
|
1032
|
-
* Use a Proxy to dynamically create font loaders for ANY font name
|
|
1033
|
-
* This allows: import { AnyGoogleFont } from "next/font/google"
|
|
1034
|
-
*/
|
|
1035
|
-
const fontProxy = new Proxy({}, {
|
|
1036
|
-
get(target, prop) {
|
|
1037
|
-
// Handle special properties
|
|
1038
|
-
if (prop === '__esModule') return true;
|
|
1039
|
-
if (prop === 'default') return fontProxy;
|
|
1040
|
-
if (typeof prop !== 'string') return undefined;
|
|
1041
|
-
|
|
1042
|
-
// Create a font loader for this font name
|
|
1043
|
-
return createFontLoader(prop);
|
|
1044
|
-
}
|
|
1045
|
-
});
|
|
1046
|
-
|
|
1047
|
-
// Export the proxy as both default and named exports
|
|
1048
|
-
export default fontProxy;
|
|
1049
|
-
|
|
1050
|
-
// Re-export through proxy for named imports
|
|
1051
|
-
export const {
|
|
1052
|
-
Fraunces, Inter, DM_Sans, DM_Serif_Text, Roboto, Open_Sans, Lato,
|
|
1053
|
-
Montserrat, Poppins, Playfair_Display, Merriweather, Raleway, Nunito,
|
|
1054
|
-
Ubuntu, Oswald, Quicksand, Work_Sans, Fira_Sans, Barlow, Mulish, Rubik,
|
|
1055
|
-
Noto_Sans, Manrope, Space_Grotesk, Geist, Geist_Mono
|
|
1056
|
-
} = fontProxy;
|
|
1057
|
-
`;
|
|
1058
|
-
|
|
1059
|
-
/**
|
|
1060
|
-
* next/font/local shim - Loads local font files
|
|
1061
|
-
* Accepts font source path and creates @font-face declaration + CSS variable
|
|
1062
|
-
*/
|
|
1063
|
-
const NEXT_FONT_LOCAL_SHIM = `
|
|
1064
|
-
const loadedLocalFonts = new Set();
|
|
1065
|
-
|
|
1066
|
-
function localFont(options = {}) {
|
|
1067
|
-
const {
|
|
1068
|
-
src,
|
|
1069
|
-
weight,
|
|
1070
|
-
style = 'normal',
|
|
1071
|
-
variable,
|
|
1072
|
-
display = 'swap',
|
|
1073
|
-
fallback = ['sans-serif'],
|
|
1074
|
-
declarations = [],
|
|
1075
|
-
adjustFontFallback = true
|
|
1076
|
-
} = options;
|
|
1077
|
-
|
|
1078
|
-
// Determine font family name from variable or src
|
|
1079
|
-
const familyName = variable
|
|
1080
|
-
? variable.replace('--', '').replace(/-/g, ' ')
|
|
1081
|
-
: 'local-font-' + Math.random().toString(36).slice(2, 8);
|
|
1082
|
-
|
|
1083
|
-
const fontKey = familyName + '-' + (variable || 'default');
|
|
1084
|
-
if (typeof document !== 'undefined' && !loadedLocalFonts.has(fontKey)) {
|
|
1085
|
-
loadedLocalFonts.add(fontKey);
|
|
1086
|
-
|
|
1087
|
-
// Build @font-face declarations
|
|
1088
|
-
let fontFaces = '';
|
|
1089
|
-
|
|
1090
|
-
if (typeof src === 'string') {
|
|
1091
|
-
// Single source
|
|
1092
|
-
fontFaces = '@font-face {\\n' +
|
|
1093
|
-
' font-family: "' + familyName + '";\\n' +
|
|
1094
|
-
' src: url("' + src + '");\\n' +
|
|
1095
|
-
' font-weight: ' + (weight || '400') + ';\\n' +
|
|
1096
|
-
' font-style: ' + style + ';\\n' +
|
|
1097
|
-
' font-display: ' + display + ';\\n' +
|
|
1098
|
-
'}';
|
|
1099
|
-
} else if (Array.isArray(src)) {
|
|
1100
|
-
// Multiple sources (different weights/styles)
|
|
1101
|
-
fontFaces = src.map(function(s) {
|
|
1102
|
-
const path = typeof s === 'string' ? s : s.path;
|
|
1103
|
-
const w = (typeof s === 'object' && s.weight) || weight || '400';
|
|
1104
|
-
const st = (typeof s === 'object' && s.style) || style;
|
|
1105
|
-
return '@font-face {\\n' +
|
|
1106
|
-
' font-family: "' + familyName + '";\\n' +
|
|
1107
|
-
' src: url("' + path + '");\\n' +
|
|
1108
|
-
' font-weight: ' + w + ';\\n' +
|
|
1109
|
-
' font-style: ' + st + ';\\n' +
|
|
1110
|
-
' font-display: ' + display + ';\\n' +
|
|
1111
|
-
'}';
|
|
1112
|
-
}).join('\\n');
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
// Inject font-face CSS
|
|
1116
|
-
if (fontFaces) {
|
|
1117
|
-
var styleEl = document.createElement('style');
|
|
1118
|
-
styleEl.setAttribute('data-local-font', fontKey);
|
|
1119
|
-
styleEl.textContent = fontFaces;
|
|
1120
|
-
document.head.appendChild(styleEl);
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
// Inject CSS variable at :root level
|
|
1124
|
-
if (variable) {
|
|
1125
|
-
var varStyle = document.createElement('style');
|
|
1126
|
-
varStyle.setAttribute('data-font-var', variable);
|
|
1127
|
-
varStyle.textContent = ':root { ' + variable + ': "' + familyName + '", ' + fallback.join(', ') + '; }';
|
|
1128
|
-
document.head.appendChild(varStyle);
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
const className = variable
|
|
1133
|
-
? variable.replace('--', '__')
|
|
1134
|
-
: '__font-' + familyName.toLowerCase().replace(/\\s+/g, '-');
|
|
1135
|
-
|
|
1136
|
-
return {
|
|
1137
|
-
className,
|
|
1138
|
-
variable: className,
|
|
1139
|
-
style: {
|
|
1140
|
-
fontFamily: '"' + familyName + '", ' + fallback.join(', ')
|
|
1141
|
-
}
|
|
1142
|
-
};
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
export default localFont;
|
|
1146
|
-
export { localFont };
|
|
1147
|
-
`;
|
|
1148
|
-
|
|
1149
112
|
/**
|
|
1150
113
|
* NextDevServer - A lightweight Next.js-compatible development server
|
|
1151
114
|
*
|
|
@@ -1270,82 +233,45 @@ export class NextDevServer extends DevServer {
|
|
|
1270
233
|
}
|
|
1271
234
|
|
|
1272
235
|
/**
|
|
1273
|
-
* Load
|
|
1274
|
-
* 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
|
|
1275
237
|
*/
|
|
1276
|
-
private
|
|
1277
|
-
// If explicitly provided in options, use it
|
|
238
|
+
private loadConfigStringValue(key: string, optionValue?: string): string {
|
|
1278
239
|
if (optionValue !== undefined) {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
this.assetPrefix = this.assetPrefix.slice(0, -1);
|
|
1283
|
-
}
|
|
1284
|
-
return;
|
|
240
|
+
let val = optionValue.startsWith('/') ? optionValue : `/${optionValue}`;
|
|
241
|
+
if (val.endsWith('/')) val = val.slice(0, -1);
|
|
242
|
+
return val;
|
|
1285
243
|
}
|
|
1286
244
|
|
|
1287
|
-
// Try to auto-detect from next.config.ts or next.config.js
|
|
1288
245
|
try {
|
|
1289
|
-
const configFiles
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
// Normalize: ensure it starts with / and doesn't end with /
|
|
1304
|
-
if (!prefix.startsWith('/')) {
|
|
1305
|
-
prefix = `/${prefix}`;
|
|
1306
|
-
}
|
|
1307
|
-
if (prefix.endsWith('/')) {
|
|
1308
|
-
prefix = prefix.slice(0, -1);
|
|
1309
|
-
}
|
|
1310
|
-
this.assetPrefix = prefix;
|
|
1311
|
-
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;
|
|
1312
260
|
}
|
|
1313
261
|
}
|
|
1314
|
-
} catch
|
|
262
|
+
} catch {
|
|
1315
263
|
// Silently ignore config parse errors
|
|
1316
264
|
}
|
|
265
|
+
|
|
266
|
+
return '';
|
|
1317
267
|
}
|
|
1318
268
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
*/
|
|
1323
|
-
private loadBasePath(optionValue?: string): void {
|
|
1324
|
-
if (optionValue !== undefined) {
|
|
1325
|
-
this.basePath = optionValue.startsWith('/') ? optionValue : `/${optionValue}`;
|
|
1326
|
-
if (this.basePath.endsWith('/')) {
|
|
1327
|
-
this.basePath = this.basePath.slice(0, -1);
|
|
1328
|
-
}
|
|
1329
|
-
return;
|
|
1330
|
-
}
|
|
269
|
+
private loadAssetPrefix(optionValue?: string): void {
|
|
270
|
+
this.assetPrefix = this.loadConfigStringValue('assetPrefix', optionValue);
|
|
271
|
+
}
|
|
1331
272
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
for (const configPath of configFiles) {
|
|
1335
|
-
if (!this.vfs.existsSync(configPath)) continue;
|
|
1336
|
-
const content = this.vfs.readFileSync(configPath, 'utf-8');
|
|
1337
|
-
const match = content.match(/basePath\s*:\s*["']([^"']+)["']/);
|
|
1338
|
-
if (match) {
|
|
1339
|
-
let bp = match[1];
|
|
1340
|
-
if (!bp.startsWith('/')) bp = `/${bp}`;
|
|
1341
|
-
if (bp.endsWith('/')) bp = bp.slice(0, -1);
|
|
1342
|
-
this.basePath = bp;
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
} catch {
|
|
1347
|
-
// Silently ignore config parse errors
|
|
1348
|
-
}
|
|
273
|
+
private loadBasePath(optionValue?: string): void {
|
|
274
|
+
this.basePath = this.loadConfigStringValue('basePath', optionValue);
|
|
1349
275
|
}
|
|
1350
276
|
|
|
1351
277
|
/**
|
|
@@ -1731,12 +657,18 @@ export class NextDevServer extends DevServer {
|
|
|
1731
657
|
* Maps /_next/app/app/about/page.js → /app/about/page.tsx (transformed)
|
|
1732
658
|
*/
|
|
1733
659
|
private async serveAppComponent(pathname: string): Promise<ResponseData> {
|
|
1734
|
-
// Extract the file path from /_next/app
|
|
1735
|
-
const
|
|
1736
|
-
|
|
1737
|
-
|
|
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$/, '');
|
|
1738
671
|
|
|
1739
|
-
// Try different extensions
|
|
1740
672
|
const extensions = ['.tsx', '.jsx', '.ts', '.js'];
|
|
1741
673
|
for (const ext of extensions) {
|
|
1742
674
|
const fullPath = filePath + ext;
|
|
@@ -2775,6 +1707,18 @@ export class NextDevServer extends DevServer {
|
|
|
2775
1707
|
return tryPath(this.appDir, segments, layouts, {});
|
|
2776
1708
|
}
|
|
2777
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
|
+
|
|
2778
1722
|
/**
|
|
2779
1723
|
* Generate HTML for App Router with nested layouts
|
|
2780
1724
|
*/
|
|
@@ -2782,394 +1726,9 @@ export class NextDevServer extends DevServer {
|
|
|
2782
1726
|
route: AppRoute,
|
|
2783
1727
|
pathname: string
|
|
2784
1728
|
): Promise<string> {
|
|
2785
|
-
|
|
2786
|
-
const virtualPrefix = `/__virtual__/${this.port}`;
|
|
2787
|
-
|
|
2788
|
-
// Check for global CSS files
|
|
2789
|
-
const globalCssLinks: string[] = [];
|
|
2790
|
-
const cssLocations = ['/app/globals.css', '/styles/globals.css', '/styles/global.css'];
|
|
2791
|
-
for (const cssPath of cssLocations) {
|
|
2792
|
-
if (this.exists(cssPath)) {
|
|
2793
|
-
globalCssLinks.push(`<link rel="stylesheet" href="${virtualPrefix}${cssPath}">`);
|
|
2794
|
-
}
|
|
2795
|
-
}
|
|
2796
|
-
|
|
2797
|
-
// Build the nested component structure
|
|
2798
|
-
// Layouts wrap the page from outside in
|
|
2799
|
-
const pageModulePath = virtualPrefix + route.page; // route.page already starts with /
|
|
2800
|
-
const layoutImports = route.layouts
|
|
2801
|
-
.map((layout, i) => `import Layout${i} from '${virtualPrefix}${layout}';`)
|
|
2802
|
-
.join('\n ');
|
|
2803
|
-
|
|
2804
|
-
// Build convention file paths for the inline script
|
|
2805
|
-
const loadingModulePath = route.loading ? `${virtualPrefix}${route.loading}` : '';
|
|
2806
|
-
const errorModulePath = route.error ? `${virtualPrefix}${route.error}` : '';
|
|
2807
|
-
const notFoundModulePath = route.notFound ? `${virtualPrefix}${route.notFound}` : '';
|
|
2808
|
-
|
|
2809
|
-
// Build nested JSX: Layout0 > Layout1 > ... > Page
|
|
2810
|
-
let nestedJsx = 'React.createElement(Page)';
|
|
2811
|
-
for (let i = route.layouts.length - 1; i >= 0; i--) {
|
|
2812
|
-
nestedJsx = `React.createElement(Layout${i}, null, ${nestedJsx})`;
|
|
2813
|
-
}
|
|
2814
|
-
|
|
2815
|
-
// Generate env script for NEXT_PUBLIC_* variables
|
|
2816
|
-
const envScript = this.generateEnvScript();
|
|
2817
|
-
|
|
2818
|
-
// Load Tailwind config if available (must be injected BEFORE CDN script)
|
|
2819
|
-
const tailwindConfigScript = await this.loadTailwindConfigIfNeeded();
|
|
2820
|
-
|
|
2821
|
-
return `<!DOCTYPE html>
|
|
2822
|
-
<html lang="en">
|
|
2823
|
-
<head>
|
|
2824
|
-
<meta charset="UTF-8">
|
|
2825
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2826
|
-
<base href="${virtualPrefix}/">
|
|
2827
|
-
<title>Next.js App</title>
|
|
2828
|
-
${envScript}
|
|
2829
|
-
${TAILWIND_CDN_SCRIPT}
|
|
2830
|
-
${tailwindConfigScript}
|
|
2831
|
-
${CORS_PROXY_SCRIPT}
|
|
2832
|
-
${globalCssLinks.join('\n ')}
|
|
2833
|
-
${REACT_REFRESH_PREAMBLE}
|
|
2834
|
-
<script type="importmap">
|
|
2835
|
-
{
|
|
2836
|
-
"imports": {
|
|
2837
|
-
"react": "https://esm.sh/react@18.2.0?dev",
|
|
2838
|
-
"react/": "https://esm.sh/react@18.2.0&dev/",
|
|
2839
|
-
"react-dom": "https://esm.sh/react-dom@18.2.0?dev",
|
|
2840
|
-
"react-dom/": "https://esm.sh/react-dom@18.2.0&dev/",
|
|
2841
|
-
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client?dev",
|
|
2842
|
-
"convex/react": "https://esm.sh/convex@1.21.0/react?external=react",
|
|
2843
|
-
"convex/server": "https://esm.sh/convex@1.21.0/server",
|
|
2844
|
-
"convex/values": "https://esm.sh/convex@1.21.0/values",
|
|
2845
|
-
"convex/_generated/api": "${virtualPrefix}/convex/_generated/api.ts",
|
|
2846
|
-
"ai": "https://esm.sh/ai@4?external=react",
|
|
2847
|
-
"ai/react": "https://esm.sh/ai@4/react?external=react",
|
|
2848
|
-
"@ai-sdk/openai": "https://esm.sh/@ai-sdk/openai@1",
|
|
2849
|
-
"next/link": "${virtualPrefix}/_next/shims/link.js",
|
|
2850
|
-
"next/router": "${virtualPrefix}/_next/shims/router.js",
|
|
2851
|
-
"next/head": "${virtualPrefix}/_next/shims/head.js",
|
|
2852
|
-
"next/navigation": "${virtualPrefix}/_next/shims/navigation.js",
|
|
2853
|
-
"next/image": "${virtualPrefix}/_next/shims/image.js",
|
|
2854
|
-
"next/dynamic": "${virtualPrefix}/_next/shims/dynamic.js",
|
|
2855
|
-
"next/script": "${virtualPrefix}/_next/shims/script.js",
|
|
2856
|
-
"next/font/google": "${virtualPrefix}/_next/shims/font/google.js",
|
|
2857
|
-
"next/font/local": "${virtualPrefix}/_next/shims/font/local.js"
|
|
2858
|
-
}
|
|
1729
|
+
return _generateAppRouterHtml(this.htmlContext(), route, pathname);
|
|
2859
1730
|
}
|
|
2860
|
-
</script>
|
|
2861
|
-
${HMR_CLIENT_SCRIPT}
|
|
2862
|
-
</head>
|
|
2863
|
-
<body>
|
|
2864
|
-
<div id="__next"></div>
|
|
2865
|
-
<script type="module">
|
|
2866
|
-
import React from 'react';
|
|
2867
|
-
import ReactDOM from 'react-dom/client';
|
|
2868
|
-
|
|
2869
|
-
const virtualBase = '${virtualPrefix}';
|
|
2870
|
-
|
|
2871
|
-
// Initial route params (embedded by server for initial page load)
|
|
2872
|
-
const initialRouteParams = ${JSON.stringify(route.params)};
|
|
2873
|
-
const initialPathname = '${pathname}';
|
|
2874
|
-
|
|
2875
|
-
// Expose initial params for useParams() hook
|
|
2876
|
-
window.__NEXT_ROUTE_PARAMS__ = initialRouteParams;
|
|
2877
|
-
|
|
2878
|
-
// Convention file paths (loading.tsx, error.tsx, not-found.tsx)
|
|
2879
|
-
const loadingModulePath = '${loadingModulePath}';
|
|
2880
|
-
const errorModulePath = '${errorModulePath}';
|
|
2881
|
-
const notFoundModulePath = '${notFoundModulePath}';
|
|
2882
|
-
|
|
2883
|
-
// Route params cache for client-side navigation
|
|
2884
|
-
const routeParamsCache = new Map();
|
|
2885
|
-
routeParamsCache.set(initialPathname, initialRouteParams);
|
|
2886
|
-
|
|
2887
|
-
// Extract route params from server for client-side navigation
|
|
2888
|
-
async function extractRouteParams(pathname) {
|
|
2889
|
-
// Strip virtual base if present
|
|
2890
|
-
let route = pathname;
|
|
2891
|
-
if (route.startsWith(virtualBase)) {
|
|
2892
|
-
route = route.slice(virtualBase.length);
|
|
2893
|
-
}
|
|
2894
|
-
route = route.replace(/^\\/+/, '/') || '/';
|
|
2895
|
-
|
|
2896
|
-
// Check cache first
|
|
2897
|
-
if (routeParamsCache.has(route)) {
|
|
2898
|
-
return routeParamsCache.get(route);
|
|
2899
|
-
}
|
|
2900
|
-
|
|
2901
|
-
try {
|
|
2902
|
-
const response = await fetch(virtualBase + '/_next/route-info?pathname=' + encodeURIComponent(route));
|
|
2903
|
-
const info = await response.json();
|
|
2904
|
-
routeParamsCache.set(route, info.params || {});
|
|
2905
|
-
return info.params || {};
|
|
2906
|
-
} catch (e) {
|
|
2907
|
-
console.error('[Router] Failed to extract route params:', e);
|
|
2908
|
-
return {};
|
|
2909
|
-
}
|
|
2910
|
-
}
|
|
2911
|
-
|
|
2912
|
-
// Convert URL path to app router page module path
|
|
2913
|
-
function getAppPageModulePath(pathname) {
|
|
2914
|
-
let route = pathname;
|
|
2915
|
-
if (route.startsWith(virtualBase)) {
|
|
2916
|
-
route = route.slice(virtualBase.length);
|
|
2917
|
-
}
|
|
2918
|
-
route = route.replace(/^\\/+/, '/') || '/';
|
|
2919
|
-
// App Router: / -> /app/page, /about -> /app/about/page
|
|
2920
|
-
const pagePath = route === '/' ? '/app/page' : '/app' + route + '/page';
|
|
2921
|
-
return virtualBase + '/_next/app' + pagePath + '.js';
|
|
2922
|
-
}
|
|
2923
1731
|
|
|
2924
|
-
// Get layout paths for a route
|
|
2925
|
-
function getLayoutPaths(pathname) {
|
|
2926
|
-
let route = pathname;
|
|
2927
|
-
if (route.startsWith(virtualBase)) {
|
|
2928
|
-
route = route.slice(virtualBase.length);
|
|
2929
|
-
}
|
|
2930
|
-
route = route.replace(/^\\/+/, '/') || '/';
|
|
2931
|
-
|
|
2932
|
-
// Build layout paths from root to current route
|
|
2933
|
-
const layouts = [virtualBase + '/_next/app/app/layout.js'];
|
|
2934
|
-
if (route !== '/') {
|
|
2935
|
-
const segments = route.split('/').filter(Boolean);
|
|
2936
|
-
let currentPath = '/app';
|
|
2937
|
-
for (const segment of segments) {
|
|
2938
|
-
currentPath += '/' + segment;
|
|
2939
|
-
layouts.push(virtualBase + '/_next/app' + currentPath + '/layout.js');
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
2942
|
-
return layouts;
|
|
2943
|
-
}
|
|
2944
|
-
|
|
2945
|
-
// Dynamic page loader
|
|
2946
|
-
async function loadPage(pathname) {
|
|
2947
|
-
const modulePath = getAppPageModulePath(pathname);
|
|
2948
|
-
try {
|
|
2949
|
-
const module = await import(/* @vite-ignore */ modulePath);
|
|
2950
|
-
return module.default;
|
|
2951
|
-
} catch (e) {
|
|
2952
|
-
console.error('[Navigation] Failed to load page:', modulePath, e);
|
|
2953
|
-
return null;
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
|
|
2957
|
-
// Load layouts (with caching)
|
|
2958
|
-
const layoutCache = new Map();
|
|
2959
|
-
async function loadLayouts(pathname) {
|
|
2960
|
-
const layoutPaths = getLayoutPaths(pathname);
|
|
2961
|
-
const layouts = [];
|
|
2962
|
-
for (const path of layoutPaths) {
|
|
2963
|
-
if (layoutCache.has(path)) {
|
|
2964
|
-
layouts.push(layoutCache.get(path));
|
|
2965
|
-
} else {
|
|
2966
|
-
try {
|
|
2967
|
-
const module = await import(/* @vite-ignore */ path);
|
|
2968
|
-
layoutCache.set(path, module.default);
|
|
2969
|
-
layouts.push(module.default);
|
|
2970
|
-
} catch (e) {
|
|
2971
|
-
// Layout might not exist for this segment, skip
|
|
2972
|
-
}
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
2975
|
-
return layouts;
|
|
2976
|
-
}
|
|
2977
|
-
|
|
2978
|
-
// Load convention components (loading.tsx, error.tsx)
|
|
2979
|
-
let LoadingComponent = null;
|
|
2980
|
-
let ErrorComponent = null;
|
|
2981
|
-
let NotFoundComponent = null;
|
|
2982
|
-
|
|
2983
|
-
async function loadConventionComponents() {
|
|
2984
|
-
if (loadingModulePath) {
|
|
2985
|
-
try {
|
|
2986
|
-
const mod = await import(/* @vite-ignore */ loadingModulePath);
|
|
2987
|
-
LoadingComponent = mod.default;
|
|
2988
|
-
} catch (e) { /* loading.tsx not available */ }
|
|
2989
|
-
}
|
|
2990
|
-
if (errorModulePath) {
|
|
2991
|
-
try {
|
|
2992
|
-
const mod = await import(/* @vite-ignore */ errorModulePath);
|
|
2993
|
-
ErrorComponent = mod.default;
|
|
2994
|
-
} catch (e) { /* error.tsx not available */ }
|
|
2995
|
-
}
|
|
2996
|
-
if (notFoundModulePath) {
|
|
2997
|
-
try {
|
|
2998
|
-
const mod = await import(/* @vite-ignore */ notFoundModulePath);
|
|
2999
|
-
NotFoundComponent = mod.default;
|
|
3000
|
-
} catch (e) { /* not-found.tsx not available */ }
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3003
|
-
await loadConventionComponents();
|
|
3004
|
-
|
|
3005
|
-
// Error boundary class component
|
|
3006
|
-
class ErrorBoundary extends React.Component {
|
|
3007
|
-
constructor(props) {
|
|
3008
|
-
super(props);
|
|
3009
|
-
this.state = { error: null };
|
|
3010
|
-
}
|
|
3011
|
-
static getDerivedStateFromError(error) {
|
|
3012
|
-
return { error };
|
|
3013
|
-
}
|
|
3014
|
-
componentDidCatch(error, info) {
|
|
3015
|
-
console.error('[ErrorBoundary]', error, info);
|
|
3016
|
-
}
|
|
3017
|
-
render() {
|
|
3018
|
-
if (this.state.error) {
|
|
3019
|
-
if (this.props.fallback) {
|
|
3020
|
-
return React.createElement(this.props.fallback, {
|
|
3021
|
-
error: this.state.error,
|
|
3022
|
-
reset: () => this.setState({ error: null })
|
|
3023
|
-
});
|
|
3024
|
-
}
|
|
3025
|
-
return React.createElement('div', { style: { color: 'red', padding: '20px' } },
|
|
3026
|
-
'Error: ' + this.state.error.message
|
|
3027
|
-
);
|
|
3028
|
-
}
|
|
3029
|
-
return this.props.children;
|
|
3030
|
-
}
|
|
3031
|
-
}
|
|
3032
|
-
|
|
3033
|
-
// Wrapper for async Server Components
|
|
3034
|
-
function AsyncComponent({ component: Component, pathname, search }) {
|
|
3035
|
-
const [content, setContent] = React.useState(null);
|
|
3036
|
-
const [error, setError] = React.useState(null);
|
|
3037
|
-
const [isNotFound, setIsNotFound] = React.useState(false);
|
|
3038
|
-
|
|
3039
|
-
React.useEffect(() => {
|
|
3040
|
-
let cancelled = false;
|
|
3041
|
-
async function render() {
|
|
3042
|
-
try {
|
|
3043
|
-
// Create searchParams as a Promise (Next.js 15 pattern)
|
|
3044
|
-
const url = new URL(window.location.href);
|
|
3045
|
-
const searchParamsObj = Object.fromEntries(url.searchParams);
|
|
3046
|
-
const searchParams = Promise.resolve(searchParamsObj);
|
|
3047
|
-
|
|
3048
|
-
// Extract route params from pathname (fetches from server for dynamic routes)
|
|
3049
|
-
const routeParams = await extractRouteParams(pathname);
|
|
3050
|
-
const params = Promise.resolve(routeParams);
|
|
3051
|
-
|
|
3052
|
-
// Call component with props like Next.js does for page components
|
|
3053
|
-
const result = Component({ searchParams, params });
|
|
3054
|
-
if (result && typeof result.then === 'function') {
|
|
3055
|
-
// It's a Promise (async component)
|
|
3056
|
-
const resolved = await result;
|
|
3057
|
-
if (!cancelled) setContent(resolved);
|
|
3058
|
-
} else {
|
|
3059
|
-
// Synchronous component - result is already JSX
|
|
3060
|
-
if (!cancelled) setContent(result);
|
|
3061
|
-
}
|
|
3062
|
-
} catch (e) {
|
|
3063
|
-
if (e && e.message === 'NEXT_NOT_FOUND') {
|
|
3064
|
-
if (!cancelled) setIsNotFound(true);
|
|
3065
|
-
} else {
|
|
3066
|
-
console.error('[AsyncComponent] Error rendering:', e);
|
|
3067
|
-
if (!cancelled) setError(e);
|
|
3068
|
-
}
|
|
3069
|
-
}
|
|
3070
|
-
}
|
|
3071
|
-
render();
|
|
3072
|
-
return () => { cancelled = true; };
|
|
3073
|
-
}, [Component, pathname, search]);
|
|
3074
|
-
|
|
3075
|
-
if (isNotFound && NotFoundComponent) {
|
|
3076
|
-
return React.createElement(NotFoundComponent);
|
|
3077
|
-
}
|
|
3078
|
-
if (isNotFound) {
|
|
3079
|
-
return React.createElement('div', { style: { padding: '20px', textAlign: 'center' } },
|
|
3080
|
-
React.createElement('h2', null, '404'),
|
|
3081
|
-
React.createElement('p', null, 'This page could not be found.')
|
|
3082
|
-
);
|
|
3083
|
-
}
|
|
3084
|
-
if (error) {
|
|
3085
|
-
if (ErrorComponent) {
|
|
3086
|
-
return React.createElement(ErrorComponent, {
|
|
3087
|
-
error: error,
|
|
3088
|
-
reset: () => setError(null)
|
|
3089
|
-
});
|
|
3090
|
-
}
|
|
3091
|
-
return React.createElement('div', { style: { color: 'red', padding: '20px' } },
|
|
3092
|
-
'Error: ' + error.message
|
|
3093
|
-
);
|
|
3094
|
-
}
|
|
3095
|
-
if (!content) {
|
|
3096
|
-
if (LoadingComponent) {
|
|
3097
|
-
return React.createElement(LoadingComponent);
|
|
3098
|
-
}
|
|
3099
|
-
return React.createElement('div', { style: { padding: '20px' } }, 'Loading...');
|
|
3100
|
-
}
|
|
3101
|
-
return content;
|
|
3102
|
-
}
|
|
3103
|
-
|
|
3104
|
-
// Router component
|
|
3105
|
-
function Router() {
|
|
3106
|
-
const [Page, setPage] = React.useState(null);
|
|
3107
|
-
const [layouts, setLayouts] = React.useState([]);
|
|
3108
|
-
const [path, setPath] = React.useState(window.location.pathname);
|
|
3109
|
-
const [search, setSearch] = React.useState(window.location.search);
|
|
3110
|
-
|
|
3111
|
-
React.useEffect(() => {
|
|
3112
|
-
Promise.all([loadPage(path), loadLayouts(path)]).then(([P, L]) => {
|
|
3113
|
-
if (P) setPage(() => P);
|
|
3114
|
-
setLayouts(L);
|
|
3115
|
-
});
|
|
3116
|
-
}, []);
|
|
3117
|
-
|
|
3118
|
-
React.useEffect(() => {
|
|
3119
|
-
const handleNavigation = async () => {
|
|
3120
|
-
const newPath = window.location.pathname;
|
|
3121
|
-
const newSearch = window.location.search;
|
|
3122
|
-
console.log('[Router] handleNavigation called, newPath:', newPath, 'current path:', path);
|
|
3123
|
-
|
|
3124
|
-
// Always update search params
|
|
3125
|
-
if (newSearch !== search) {
|
|
3126
|
-
setSearch(newSearch);
|
|
3127
|
-
}
|
|
3128
|
-
|
|
3129
|
-
if (newPath !== path) {
|
|
3130
|
-
console.log('[Router] Path changed, loading new page...');
|
|
3131
|
-
setPath(newPath);
|
|
3132
|
-
const [P, L, routeParams] = await Promise.all([loadPage(newPath), loadLayouts(newPath), extractRouteParams(newPath)]);
|
|
3133
|
-
window.__NEXT_ROUTE_PARAMS__ = routeParams;
|
|
3134
|
-
console.log('[Router] Page loaded:', !!P, 'Layouts:', L.length);
|
|
3135
|
-
if (P) setPage(() => P);
|
|
3136
|
-
setLayouts(L);
|
|
3137
|
-
} else {
|
|
3138
|
-
console.log('[Router] Path unchanged, skipping navigation');
|
|
3139
|
-
}
|
|
3140
|
-
};
|
|
3141
|
-
window.addEventListener('popstate', handleNavigation);
|
|
3142
|
-
console.log('[Router] Added popstate listener for path:', path);
|
|
3143
|
-
return () => window.removeEventListener('popstate', handleNavigation);
|
|
3144
|
-
}, [path, search]);
|
|
3145
|
-
|
|
3146
|
-
if (!Page) return null;
|
|
3147
|
-
|
|
3148
|
-
// Use AsyncComponent wrapper to handle async Server Components
|
|
3149
|
-
// Pass search to force re-render when query params change
|
|
3150
|
-
let content = React.createElement(AsyncComponent, { component: Page, pathname: path, search: search });
|
|
3151
|
-
|
|
3152
|
-
// Wrap with error boundary if error.tsx exists
|
|
3153
|
-
if (ErrorComponent) {
|
|
3154
|
-
content = React.createElement(ErrorBoundary, { fallback: ErrorComponent }, content);
|
|
3155
|
-
}
|
|
3156
|
-
|
|
3157
|
-
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
3158
|
-
content = React.createElement(layouts[i], null, content);
|
|
3159
|
-
}
|
|
3160
|
-
return content;
|
|
3161
|
-
}
|
|
3162
|
-
|
|
3163
|
-
// Mark that we've initialized (for testing no-reload)
|
|
3164
|
-
window.__NEXT_INITIALIZED__ = Date.now();
|
|
3165
|
-
|
|
3166
|
-
ReactDOM.createRoot(document.getElementById('__next')).render(
|
|
3167
|
-
React.createElement(React.StrictMode, null, React.createElement(Router))
|
|
3168
|
-
);
|
|
3169
|
-
</script>
|
|
3170
|
-
</body>
|
|
3171
|
-
</html>`;
|
|
3172
|
-
}
|
|
3173
1732
|
|
|
3174
1733
|
/**
|
|
3175
1734
|
* Resolve URL pathname to page file
|
|
@@ -3295,176 +1854,14 @@ export class NextDevServer extends DevServer {
|
|
|
3295
1854
|
* Generate HTML shell for a page
|
|
3296
1855
|
*/
|
|
3297
1856
|
private async generatePageHtml(pageFile: string, pathname: string): Promise<string> {
|
|
3298
|
-
|
|
3299
|
-
// Without this, /pages/index.jsx would go to localhost:5173/pages/index.jsx
|
|
3300
|
-
// instead of /__virtual__/3001/pages/index.jsx
|
|
3301
|
-
const virtualPrefix = `/__virtual__/${this.port}`;
|
|
3302
|
-
const pageModulePath = virtualPrefix + pageFile; // pageFile already starts with /
|
|
3303
|
-
|
|
3304
|
-
// Check for global CSS files
|
|
3305
|
-
const globalCssLinks: string[] = [];
|
|
3306
|
-
const cssLocations = ['/styles/globals.css', '/styles/global.css', '/app/globals.css'];
|
|
3307
|
-
for (const cssPath of cssLocations) {
|
|
3308
|
-
if (this.exists(cssPath)) {
|
|
3309
|
-
globalCssLinks.push(`<link rel="stylesheet" href="${virtualPrefix}${cssPath}">`);
|
|
3310
|
-
}
|
|
3311
|
-
}
|
|
3312
|
-
|
|
3313
|
-
// Generate env script for NEXT_PUBLIC_* variables
|
|
3314
|
-
const envScript = this.generateEnvScript();
|
|
3315
|
-
|
|
3316
|
-
// Load Tailwind config if available (must be injected BEFORE CDN script)
|
|
3317
|
-
const tailwindConfigScript = await this.loadTailwindConfigIfNeeded();
|
|
3318
|
-
|
|
3319
|
-
return `<!DOCTYPE html>
|
|
3320
|
-
<html lang="en">
|
|
3321
|
-
<head>
|
|
3322
|
-
<meta charset="UTF-8">
|
|
3323
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3324
|
-
<base href="${virtualPrefix}/">
|
|
3325
|
-
<title>Next.js App</title>
|
|
3326
|
-
${envScript}
|
|
3327
|
-
${TAILWIND_CDN_SCRIPT}
|
|
3328
|
-
${tailwindConfigScript}
|
|
3329
|
-
${CORS_PROXY_SCRIPT}
|
|
3330
|
-
${globalCssLinks.join('\n ')}
|
|
3331
|
-
${REACT_REFRESH_PREAMBLE}
|
|
3332
|
-
<script type="importmap">
|
|
3333
|
-
{
|
|
3334
|
-
"imports": {
|
|
3335
|
-
"react": "https://esm.sh/react@18.2.0?dev",
|
|
3336
|
-
"react/": "https://esm.sh/react@18.2.0&dev/",
|
|
3337
|
-
"react-dom": "https://esm.sh/react-dom@18.2.0?dev",
|
|
3338
|
-
"react-dom/": "https://esm.sh/react-dom@18.2.0&dev/",
|
|
3339
|
-
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client?dev",
|
|
3340
|
-
"next/link": "${virtualPrefix}/_next/shims/link.js",
|
|
3341
|
-
"next/router": "${virtualPrefix}/_next/shims/router.js",
|
|
3342
|
-
"next/head": "${virtualPrefix}/_next/shims/head.js",
|
|
3343
|
-
"next/navigation": "${virtualPrefix}/_next/shims/navigation.js",
|
|
3344
|
-
"next/image": "${virtualPrefix}/_next/shims/image.js",
|
|
3345
|
-
"next/dynamic": "${virtualPrefix}/_next/shims/dynamic.js",
|
|
3346
|
-
"next/script": "${virtualPrefix}/_next/shims/script.js",
|
|
3347
|
-
"next/font/google": "${virtualPrefix}/_next/shims/font/google.js",
|
|
3348
|
-
"next/font/local": "${virtualPrefix}/_next/shims/font/local.js"
|
|
3349
|
-
}
|
|
3350
|
-
}
|
|
3351
|
-
</script>
|
|
3352
|
-
${HMR_CLIENT_SCRIPT}
|
|
3353
|
-
</head>
|
|
3354
|
-
<body>
|
|
3355
|
-
<div id="__next"></div>
|
|
3356
|
-
<script type="module">
|
|
3357
|
-
import React from 'react';
|
|
3358
|
-
import ReactDOM from 'react-dom/client';
|
|
3359
|
-
|
|
3360
|
-
const virtualBase = '${virtualPrefix}';
|
|
3361
|
-
|
|
3362
|
-
// Convert URL path to page module path
|
|
3363
|
-
function getPageModulePath(pathname) {
|
|
3364
|
-
let route = pathname;
|
|
3365
|
-
if (route.startsWith(virtualBase)) {
|
|
3366
|
-
route = route.slice(virtualBase.length);
|
|
3367
|
-
}
|
|
3368
|
-
route = route.replace(/^\\/+/, '/') || '/';
|
|
3369
|
-
const modulePath = route === '/' ? '/index' : route;
|
|
3370
|
-
return virtualBase + '/_next/pages' + modulePath + '.js';
|
|
3371
|
-
}
|
|
3372
|
-
|
|
3373
|
-
// Dynamic page loader
|
|
3374
|
-
async function loadPage(pathname) {
|
|
3375
|
-
const modulePath = getPageModulePath(pathname);
|
|
3376
|
-
try {
|
|
3377
|
-
const module = await import(/* @vite-ignore */ modulePath);
|
|
3378
|
-
return module.default;
|
|
3379
|
-
} catch (e) {
|
|
3380
|
-
console.error('[Navigation] Failed to load:', modulePath, e);
|
|
3381
|
-
return null;
|
|
3382
|
-
}
|
|
3383
|
-
}
|
|
3384
|
-
|
|
3385
|
-
// Router component
|
|
3386
|
-
function Router() {
|
|
3387
|
-
const [Page, setPage] = React.useState(null);
|
|
3388
|
-
const [path, setPath] = React.useState(window.location.pathname);
|
|
3389
|
-
|
|
3390
|
-
React.useEffect(() => {
|
|
3391
|
-
loadPage(path).then(C => C && setPage(() => C));
|
|
3392
|
-
}, []);
|
|
3393
|
-
|
|
3394
|
-
React.useEffect(() => {
|
|
3395
|
-
const handleNavigation = async () => {
|
|
3396
|
-
const newPath = window.location.pathname;
|
|
3397
|
-
if (newPath !== path) {
|
|
3398
|
-
setPath(newPath);
|
|
3399
|
-
const C = await loadPage(newPath);
|
|
3400
|
-
if (C) setPage(() => C);
|
|
3401
|
-
}
|
|
3402
|
-
};
|
|
3403
|
-
window.addEventListener('popstate', handleNavigation);
|
|
3404
|
-
return () => window.removeEventListener('popstate', handleNavigation);
|
|
3405
|
-
}, [path]);
|
|
3406
|
-
|
|
3407
|
-
if (!Page) return null;
|
|
3408
|
-
return React.createElement(Page);
|
|
3409
|
-
}
|
|
3410
|
-
|
|
3411
|
-
// Mark that we've initialized (for testing no-reload)
|
|
3412
|
-
window.__NEXT_INITIALIZED__ = Date.now();
|
|
3413
|
-
|
|
3414
|
-
ReactDOM.createRoot(document.getElementById('__next')).render(
|
|
3415
|
-
React.createElement(React.StrictMode, null, React.createElement(Router))
|
|
3416
|
-
);
|
|
3417
|
-
</script>
|
|
3418
|
-
</body>
|
|
3419
|
-
</html>`;
|
|
1857
|
+
return _generatePageHtml(this.htmlContext(), pageFile, pathname);
|
|
3420
1858
|
}
|
|
3421
1859
|
|
|
3422
1860
|
/**
|
|
3423
1861
|
* Serve a basic 404 page
|
|
3424
1862
|
*/
|
|
3425
1863
|
private serve404Page(): ResponseData {
|
|
3426
|
-
|
|
3427
|
-
const html = `<!DOCTYPE html>
|
|
3428
|
-
<html lang="en">
|
|
3429
|
-
<head>
|
|
3430
|
-
<meta charset="UTF-8">
|
|
3431
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3432
|
-
<base href="${virtualPrefix}/">
|
|
3433
|
-
<title>404 - Page Not Found</title>
|
|
3434
|
-
<style>
|
|
3435
|
-
body {
|
|
3436
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
3437
|
-
display: flex;
|
|
3438
|
-
flex-direction: column;
|
|
3439
|
-
align-items: center;
|
|
3440
|
-
justify-content: center;
|
|
3441
|
-
min-height: 100vh;
|
|
3442
|
-
margin: 0;
|
|
3443
|
-
background: #fafafa;
|
|
3444
|
-
}
|
|
3445
|
-
h1 { font-size: 48px; margin: 0; }
|
|
3446
|
-
p { color: #666; margin-top: 10px; }
|
|
3447
|
-
a { color: #0070f3; text-decoration: none; }
|
|
3448
|
-
a:hover { text-decoration: underline; }
|
|
3449
|
-
</style>
|
|
3450
|
-
</head>
|
|
3451
|
-
<body>
|
|
3452
|
-
<h1>404</h1>
|
|
3453
|
-
<p>This page could not be found.</p>
|
|
3454
|
-
<p><a href="/">Go back home</a></p>
|
|
3455
|
-
</body>
|
|
3456
|
-
</html>`;
|
|
3457
|
-
|
|
3458
|
-
const buffer = Buffer.from(html);
|
|
3459
|
-
return {
|
|
3460
|
-
statusCode: 404,
|
|
3461
|
-
statusMessage: 'Not Found',
|
|
3462
|
-
headers: {
|
|
3463
|
-
'Content-Type': 'text/html; charset=utf-8',
|
|
3464
|
-
'Content-Length': String(buffer.length),
|
|
3465
|
-
},
|
|
3466
|
-
body: buffer,
|
|
3467
|
-
};
|
|
1864
|
+
return _serve404Page(this.port);
|
|
3468
1865
|
}
|
|
3469
1866
|
|
|
3470
1867
|
/**
|
|
@@ -3536,8 +1933,12 @@ export class NextDevServer extends DevServer {
|
|
|
3536
1933
|
// Use filePath (with extension) for transform so loader is correctly determined
|
|
3537
1934
|
const transformed = await this.transformCode(content, filePath);
|
|
3538
1935
|
|
|
3539
|
-
// Cache the transform result
|
|
1936
|
+
// Cache the transform result (LRU eviction at 500 entries)
|
|
3540
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
|
+
}
|
|
3541
1942
|
|
|
3542
1943
|
const buffer = Buffer.from(transformed);
|
|
3543
1944
|
return {
|