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.
Files changed (64) hide show
  1. package/README.md +4 -2
  2. package/dist/CNAME +1 -0
  3. package/dist/__sw__.js +80 -84
  4. package/dist/assets/{runtime-worker-B8_LZkBX.js → runtime-worker-ujGAG2t7.js} +1278 -828
  5. package/dist/assets/runtime-worker-ujGAG2t7.js.map +1 -0
  6. package/dist/frameworks/code-transforms.d.ts.map +1 -1
  7. package/dist/frameworks/next-config-parser.d.ts +16 -0
  8. package/dist/frameworks/next-config-parser.d.ts.map +1 -0
  9. package/dist/frameworks/next-dev-server.d.ts +6 -6
  10. package/dist/frameworks/next-dev-server.d.ts.map +1 -1
  11. package/dist/frameworks/next-html-generator.d.ts +35 -0
  12. package/dist/frameworks/next-html-generator.d.ts.map +1 -0
  13. package/dist/frameworks/next-shims.d.ts +79 -0
  14. package/dist/frameworks/next-shims.d.ts.map +1 -0
  15. package/dist/index.cjs +3024 -2465
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.mjs +3336 -2787
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/og-image.png +0 -0
  22. package/dist/runtime.d.ts +26 -0
  23. package/dist/runtime.d.ts.map +1 -1
  24. package/dist/server-bridge.d.ts +2 -0
  25. package/dist/server-bridge.d.ts.map +1 -1
  26. package/dist/shims/crypto.d.ts +2 -0
  27. package/dist/shims/crypto.d.ts.map +1 -1
  28. package/dist/shims/esbuild.d.ts.map +1 -1
  29. package/dist/shims/fs.d.ts.map +1 -1
  30. package/dist/shims/http.d.ts +29 -0
  31. package/dist/shims/http.d.ts.map +1 -1
  32. package/dist/shims/path.d.ts.map +1 -1
  33. package/dist/shims/stream.d.ts.map +1 -1
  34. package/dist/shims/vfs-adapter.d.ts.map +1 -1
  35. package/dist/shims/ws.d.ts +2 -0
  36. package/dist/shims/ws.d.ts.map +1 -1
  37. package/dist/types/package-json.d.ts +1 -0
  38. package/dist/types/package-json.d.ts.map +1 -1
  39. package/dist/utils/binary-encoding.d.ts +13 -0
  40. package/dist/utils/binary-encoding.d.ts.map +1 -0
  41. package/dist/virtual-fs.d.ts.map +1 -1
  42. package/package.json +4 -4
  43. package/src/convex-app-demo-entry.ts +229 -35
  44. package/src/frameworks/code-transforms.ts +5 -1
  45. package/src/frameworks/next-config-parser.ts +140 -0
  46. package/src/frameworks/next-dev-server.ts +76 -1675
  47. package/src/frameworks/next-html-generator.ts +597 -0
  48. package/src/frameworks/next-shims.ts +1050 -0
  49. package/src/frameworks/tailwind-config-loader.ts +1 -1
  50. package/src/index.ts +2 -0
  51. package/src/runtime.ts +271 -25
  52. package/src/server-bridge.ts +61 -28
  53. package/src/shims/crypto.ts +13 -0
  54. package/src/shims/esbuild.ts +4 -1
  55. package/src/shims/fs.ts +9 -11
  56. package/src/shims/http.ts +312 -3
  57. package/src/shims/path.ts +6 -13
  58. package/src/shims/stream.ts +12 -26
  59. package/src/shims/vfs-adapter.ts +5 -2
  60. package/src/shims/ws.ts +95 -2
  61. package/src/types/package-json.ts +1 -0
  62. package/src/utils/binary-encoding.ts +43 -0
  63. package/src/virtual-fs.ts +7 -15
  64. package/dist/assets/runtime-worker-B8_LZkBX.js.map +0 -1
@@ -0,0 +1,597 @@
1
+ /**
2
+ * Next.js HTML page generation
3
+ * Standalone functions extracted from NextDevServer for generating
4
+ * App Router HTML, Pages Router HTML, and 404 pages.
5
+ */
6
+
7
+ import { Buffer } from '../shims/stream';
8
+ import { ResponseData } from '../dev-server';
9
+ import {
10
+ TAILWIND_CDN_SCRIPT,
11
+ CORS_PROXY_SCRIPT,
12
+ REACT_REFRESH_PREAMBLE,
13
+ HMR_CLIENT_SCRIPT,
14
+ } from './next-shims';
15
+
16
+ /** Resolved App Router route with page, layouts, and UI convention files */
17
+ export interface AppRoute {
18
+ page: string;
19
+ layouts: string[];
20
+ params: Record<string, string | string[]>;
21
+ loading?: string;
22
+ error?: string;
23
+ notFound?: string;
24
+ }
25
+
26
+ /** Context needed by HTML generation functions */
27
+ export interface HtmlGeneratorContext {
28
+ port: number;
29
+ exists: (path: string) => boolean;
30
+ generateEnvScript: () => string;
31
+ loadTailwindConfigIfNeeded: () => Promise<string>;
32
+ }
33
+
34
+ /**
35
+ * Generate HTML for App Router with nested layouts
36
+ */
37
+ export async function generateAppRouterHtml(
38
+ ctx: HtmlGeneratorContext,
39
+ route: AppRoute,
40
+ pathname: string
41
+ ): Promise<string> {
42
+ // Use virtual server prefix for all file imports so the service worker can intercept them
43
+ const virtualPrefix = `/__virtual__/${ctx.port}`;
44
+
45
+ // Check for global CSS files
46
+ const globalCssLinks: string[] = [];
47
+ const cssLocations = ['/app/globals.css', '/styles/globals.css', '/styles/global.css'];
48
+ for (const cssPath of cssLocations) {
49
+ if (ctx.exists(cssPath)) {
50
+ globalCssLinks.push(`<link rel="stylesheet" href="${virtualPrefix}${cssPath}">`);
51
+ }
52
+ }
53
+
54
+ // Build the nested component structure
55
+ // Layouts wrap the page from outside in
56
+ const pageModulePath = virtualPrefix + route.page; // route.page already starts with /
57
+ const layoutImports = route.layouts
58
+ .map((layout, i) => `import Layout${i} from '${virtualPrefix}${layout}';`)
59
+ .join('\n ');
60
+
61
+ // Build convention file paths for the inline script
62
+ const loadingModulePath = route.loading ? `${virtualPrefix}${route.loading}` : '';
63
+ const errorModulePath = route.error ? `${virtualPrefix}${route.error}` : '';
64
+ const notFoundModulePath = route.notFound ? `${virtualPrefix}${route.notFound}` : '';
65
+
66
+ // Build nested JSX: Layout0 > Layout1 > ... > Page
67
+ let nestedJsx = 'React.createElement(Page)';
68
+ for (let i = route.layouts.length - 1; i >= 0; i--) {
69
+ nestedJsx = `React.createElement(Layout${i}, null, ${nestedJsx})`;
70
+ }
71
+
72
+ // Generate env script for NEXT_PUBLIC_* variables
73
+ const envScript = ctx.generateEnvScript();
74
+
75
+ // Load Tailwind config if available (must be injected BEFORE CDN script)
76
+ const tailwindConfigScript = await ctx.loadTailwindConfigIfNeeded();
77
+
78
+ return `<!DOCTYPE html>
79
+ <html lang="en">
80
+ <head>
81
+ <meta charset="UTF-8">
82
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
83
+ <base href="${virtualPrefix}/">
84
+ <title>Next.js App</title>
85
+ ${envScript}
86
+ ${TAILWIND_CDN_SCRIPT}
87
+ ${tailwindConfigScript}
88
+ ${CORS_PROXY_SCRIPT}
89
+ ${globalCssLinks.join('\n ')}
90
+ <script type="importmap">
91
+ {
92
+ "imports": {
93
+ "react": "https://esm.sh/react@18.2.0?dev",
94
+ "react/": "https://esm.sh/react@18.2.0&dev/",
95
+ "react-dom": "https://esm.sh/react-dom@18.2.0?dev",
96
+ "react-dom/": "https://esm.sh/react-dom@18.2.0&dev/",
97
+ "react-dom/client": "https://esm.sh/react-dom@18.2.0/client?dev",
98
+ "convex/react": "https://esm.sh/convex@1.21.0/react?external=react",
99
+ "convex/server": "https://esm.sh/convex@1.21.0/server",
100
+ "convex/values": "https://esm.sh/convex@1.21.0/values",
101
+ "convex/_generated/api": "${virtualPrefix}/convex/_generated/api.ts",
102
+ "ai": "https://esm.sh/ai@4?external=react",
103
+ "ai/react": "https://esm.sh/ai@4/react?external=react",
104
+ "@ai-sdk/openai": "https://esm.sh/@ai-sdk/openai@1",
105
+ "next/link": "${virtualPrefix}/_next/shims/link.js",
106
+ "next/router": "${virtualPrefix}/_next/shims/router.js",
107
+ "next/head": "${virtualPrefix}/_next/shims/head.js",
108
+ "next/navigation": "${virtualPrefix}/_next/shims/navigation.js",
109
+ "next/image": "${virtualPrefix}/_next/shims/image.js",
110
+ "next/dynamic": "${virtualPrefix}/_next/shims/dynamic.js",
111
+ "next/script": "${virtualPrefix}/_next/shims/script.js",
112
+ "next/font/google": "${virtualPrefix}/_next/shims/font/google.js",
113
+ "next/font/local": "${virtualPrefix}/_next/shims/font/local.js"
114
+ }
115
+ }
116
+ </script>
117
+ ${REACT_REFRESH_PREAMBLE}
118
+ ${HMR_CLIENT_SCRIPT}
119
+ </head>
120
+ <body>
121
+ <div id="__next"></div>
122
+ <script type="module">
123
+ import React from 'react';
124
+ import ReactDOM from 'react-dom/client';
125
+
126
+ const virtualBase = '${virtualPrefix}';
127
+
128
+ // Initial route params (embedded by server for initial page load)
129
+ const initialRouteParams = ${JSON.stringify(route.params)};
130
+ const initialPathname = '${pathname}';
131
+
132
+ // Expose initial params for useParams() hook
133
+ window.__NEXT_ROUTE_PARAMS__ = initialRouteParams;
134
+
135
+ // Convention file paths (loading.tsx, error.tsx, not-found.tsx)
136
+ const loadingModulePath = '${loadingModulePath}';
137
+ const errorModulePath = '${errorModulePath}';
138
+ const notFoundModulePath = '${notFoundModulePath}';
139
+
140
+ // Route params cache for client-side navigation
141
+ const routeParamsCache = new Map();
142
+ routeParamsCache.set(initialPathname, initialRouteParams);
143
+
144
+ // Extract route params from server for client-side navigation
145
+ async function extractRouteParams(pathname) {
146
+ // Strip virtual base if present
147
+ let route = pathname;
148
+ if (route.startsWith(virtualBase)) {
149
+ route = route.slice(virtualBase.length);
150
+ }
151
+ route = route.replace(/^\\/+/, '/') || '/';
152
+
153
+ // Check cache first
154
+ if (routeParamsCache.has(route)) {
155
+ return routeParamsCache.get(route);
156
+ }
157
+
158
+ try {
159
+ const response = await fetch(virtualBase + '/_next/route-info?pathname=' + encodeURIComponent(route));
160
+ const info = await response.json();
161
+ routeParamsCache.set(route, info.params || {});
162
+ return info.params || {};
163
+ } catch (e) {
164
+ console.error('[Router] Failed to extract route params:', e);
165
+ return {};
166
+ }
167
+ }
168
+
169
+ // Convert URL path to app router page module path
170
+ function getAppPageModulePath(pathname) {
171
+ let route = pathname;
172
+ if (route.startsWith(virtualBase)) {
173
+ route = route.slice(virtualBase.length);
174
+ }
175
+ route = route.replace(/^\\/+/, '/') || '/';
176
+ // App Router: / -> /app/page, /about -> /app/about/page
177
+ const pagePath = route === '/' ? '/app/page' : '/app' + route + '/page';
178
+ return virtualBase + '/_next/app' + pagePath + '.js';
179
+ }
180
+
181
+ // Get layout paths for a route
182
+ function getLayoutPaths(pathname) {
183
+ let route = pathname;
184
+ if (route.startsWith(virtualBase)) {
185
+ route = route.slice(virtualBase.length);
186
+ }
187
+ route = route.replace(/^\\/+/, '/') || '/';
188
+
189
+ // Build layout paths from root to current route
190
+ const layouts = [virtualBase + '/_next/app/app/layout.js'];
191
+ if (route !== '/') {
192
+ const segments = route.split('/').filter(Boolean);
193
+ let currentPath = '/app';
194
+ for (const segment of segments) {
195
+ currentPath += '/' + segment;
196
+ layouts.push(virtualBase + '/_next/app' + currentPath + '/layout.js');
197
+ }
198
+ }
199
+ return layouts;
200
+ }
201
+
202
+ // Dynamic page loader with retry (SW may need time to recover after idle termination)
203
+ async function loadPage(pathname) {
204
+ const modulePath = getAppPageModulePath(pathname);
205
+ for (let attempt = 0; attempt < 3; attempt++) {
206
+ try {
207
+ const module = await import(/* @vite-ignore */ modulePath + (attempt > 0 ? '?retry=' + attempt : ''));
208
+ return module.default;
209
+ } catch (e) {
210
+ console.warn('[Navigation] Load attempt ' + (attempt + 1) + ' failed:', modulePath, e.message);
211
+ if (attempt < 2) await new Promise(r => setTimeout(r, 1000));
212
+ }
213
+ }
214
+ console.error('[Navigation] Failed to load page after 3 attempts:', modulePath);
215
+ return null;
216
+ }
217
+
218
+ // Load layouts (with caching)
219
+ const layoutCache = new Map();
220
+ async function loadLayouts(pathname) {
221
+ const layoutPaths = getLayoutPaths(pathname);
222
+ const layouts = [];
223
+ for (const path of layoutPaths) {
224
+ if (layoutCache.has(path)) {
225
+ layouts.push(layoutCache.get(path));
226
+ } else {
227
+ try {
228
+ const module = await import(/* @vite-ignore */ path);
229
+ layoutCache.set(path, module.default);
230
+ layouts.push(module.default);
231
+ } catch (e) {
232
+ // Layout might not exist for this segment, skip
233
+ }
234
+ }
235
+ }
236
+ return layouts;
237
+ }
238
+
239
+ // Load convention components (loading.tsx, error.tsx)
240
+ let LoadingComponent = null;
241
+ let ErrorComponent = null;
242
+ let NotFoundComponent = null;
243
+
244
+ async function loadConventionComponents() {
245
+ if (loadingModulePath) {
246
+ try {
247
+ const mod = await import(/* @vite-ignore */ loadingModulePath);
248
+ LoadingComponent = mod.default;
249
+ } catch (e) { /* loading.tsx not available */ }
250
+ }
251
+ if (errorModulePath) {
252
+ try {
253
+ const mod = await import(/* @vite-ignore */ errorModulePath);
254
+ ErrorComponent = mod.default;
255
+ } catch (e) { /* error.tsx not available */ }
256
+ }
257
+ if (notFoundModulePath) {
258
+ try {
259
+ const mod = await import(/* @vite-ignore */ notFoundModulePath);
260
+ NotFoundComponent = mod.default;
261
+ } catch (e) { /* not-found.tsx not available */ }
262
+ }
263
+ }
264
+ await loadConventionComponents();
265
+
266
+ // Error boundary class component
267
+ class ErrorBoundary extends React.Component {
268
+ constructor(props) {
269
+ super(props);
270
+ this.state = { error: null };
271
+ }
272
+ static getDerivedStateFromError(error) {
273
+ return { error };
274
+ }
275
+ componentDidCatch(error, info) {
276
+ console.error('[ErrorBoundary]', error, info);
277
+ }
278
+ render() {
279
+ if (this.state.error) {
280
+ if (this.props.fallback) {
281
+ return React.createElement(this.props.fallback, {
282
+ error: this.state.error,
283
+ reset: () => this.setState({ error: null })
284
+ });
285
+ }
286
+ return React.createElement('div', { style: { color: 'red', padding: '20px' } },
287
+ 'Error: ' + this.state.error.message
288
+ );
289
+ }
290
+ return this.props.children;
291
+ }
292
+ }
293
+
294
+ // Wrapper that provides searchParams/params props and handles errors
295
+ function PageWrapper({ component: Component, pathname, search }) {
296
+ const [searchParams, setSearchParams] = React.useState(() => {
297
+ const url = new URL(window.location.href);
298
+ return Promise.resolve(Object.fromEntries(url.searchParams));
299
+ });
300
+ const [params, setParams] = React.useState(() => Promise.resolve(initialRouteParams));
301
+ const [isNotFound, setIsNotFound] = React.useState(false);
302
+
303
+ React.useEffect(() => {
304
+ // Update searchParams when search changes
305
+ const url = new URL(window.location.href);
306
+ setSearchParams(Promise.resolve(Object.fromEntries(url.searchParams)));
307
+ }, [search]);
308
+
309
+ React.useEffect(() => {
310
+ // Update route params when pathname changes
311
+ let cancelled = false;
312
+ extractRouteParams(pathname).then(routeParams => {
313
+ if (!cancelled) setParams(Promise.resolve(routeParams));
314
+ });
315
+ return () => { cancelled = true; };
316
+ }, [pathname]);
317
+
318
+ if (isNotFound && NotFoundComponent) {
319
+ return React.createElement(NotFoundComponent);
320
+ }
321
+ if (isNotFound) {
322
+ return React.createElement('div', { style: { padding: '20px', textAlign: 'center' } },
323
+ React.createElement('h2', null, '404'),
324
+ React.createElement('p', null, 'This page could not be found.')
325
+ );
326
+ }
327
+
328
+ // Render the component via createElement so hooks work correctly
329
+ try {
330
+ return React.createElement(Component, { searchParams, params });
331
+ } catch (e) {
332
+ if (e && e.message === 'NEXT_NOT_FOUND') {
333
+ // Will re-render with notFound on next tick
334
+ if (!isNotFound) setIsNotFound(true);
335
+ return null;
336
+ }
337
+ throw e; // Let ErrorBoundary handle it
338
+ }
339
+ }
340
+
341
+ // Router component
342
+ function Router() {
343
+ const [Page, setPage] = React.useState(null);
344
+ const [layouts, setLayouts] = React.useState([]);
345
+ const [path, setPath] = React.useState(window.location.pathname);
346
+ const [search, setSearch] = React.useState(window.location.search);
347
+
348
+ React.useEffect(() => {
349
+ Promise.all([loadPage(path), loadLayouts(path)]).then(([P, L]) => {
350
+ if (P) setPage(() => P);
351
+ setLayouts(L);
352
+ });
353
+ }, []);
354
+
355
+ React.useEffect(() => {
356
+ const handleNavigation = async () => {
357
+ const newPath = window.location.pathname;
358
+ const newSearch = window.location.search;
359
+ console.log('[Router] handleNavigation called, newPath:', newPath, 'current path:', path);
360
+
361
+ // Always update search params
362
+ if (newSearch !== search) {
363
+ setSearch(newSearch);
364
+ }
365
+
366
+ if (newPath !== path) {
367
+ console.log('[Router] Path changed, loading new page...');
368
+ setPath(newPath);
369
+ const [P, L, routeParams] = await Promise.all([loadPage(newPath), loadLayouts(newPath), extractRouteParams(newPath)]);
370
+ window.__NEXT_ROUTE_PARAMS__ = routeParams;
371
+ console.log('[Router] Page loaded:', !!P, 'Layouts:', L.length);
372
+ if (P) setPage(() => P);
373
+ setLayouts(L);
374
+ } else {
375
+ console.log('[Router] Path unchanged, skipping navigation');
376
+ }
377
+ };
378
+ window.addEventListener('popstate', handleNavigation);
379
+ console.log('[Router] Added popstate listener for path:', path);
380
+ return () => window.removeEventListener('popstate', handleNavigation);
381
+ }, [path, search]);
382
+
383
+ if (!Page) return null;
384
+
385
+ // Render page via PageWrapper so hooks work correctly
386
+ // Pass search to force re-render when query params change
387
+ let content = React.createElement(PageWrapper, { component: Page, pathname: path, search: search });
388
+
389
+ // Wrap with loading.tsx Suspense fallback if it exists
390
+ if (LoadingComponent) {
391
+ content = React.createElement(React.Suspense,
392
+ { fallback: React.createElement(LoadingComponent) },
393
+ content
394
+ );
395
+ }
396
+
397
+ // Wrap with error boundary if error.tsx exists
398
+ if (ErrorComponent) {
399
+ content = React.createElement(ErrorBoundary, { fallback: ErrorComponent }, content);
400
+ }
401
+
402
+ for (let i = layouts.length - 1; i >= 0; i--) {
403
+ content = React.createElement(layouts[i], null, content);
404
+ }
405
+ return content;
406
+ }
407
+
408
+ // Mark that we've initialized (for testing no-reload)
409
+ window.__NEXT_INITIALIZED__ = Date.now();
410
+
411
+ ReactDOM.createRoot(document.getElementById('__next')).render(
412
+ React.createElement(React.StrictMode, null, React.createElement(Router))
413
+ );
414
+ </script>
415
+ </body>
416
+ </html>`;
417
+ }
418
+
419
+ /**
420
+ * Generate HTML shell for a Pages Router page
421
+ */
422
+ export async function generatePageHtml(
423
+ ctx: HtmlGeneratorContext,
424
+ pageFile: string,
425
+ pathname: string
426
+ ): Promise<string> {
427
+ // Use virtual server prefix for all file imports so the service worker can intercept them
428
+ // Without this, /pages/index.jsx would go to localhost:5173/pages/index.jsx
429
+ // instead of /__virtual__/3001/pages/index.jsx
430
+ const virtualPrefix = `/__virtual__/${ctx.port}`;
431
+ const pageModulePath = virtualPrefix + pageFile; // pageFile already starts with /
432
+
433
+ // Check for global CSS files
434
+ const globalCssLinks: string[] = [];
435
+ const cssLocations = ['/styles/globals.css', '/styles/global.css', '/app/globals.css'];
436
+ for (const cssPath of cssLocations) {
437
+ if (ctx.exists(cssPath)) {
438
+ globalCssLinks.push(`<link rel="stylesheet" href="${virtualPrefix}${cssPath}">`);
439
+ }
440
+ }
441
+
442
+ // Generate env script for NEXT_PUBLIC_* variables
443
+ const envScript = ctx.generateEnvScript();
444
+
445
+ // Load Tailwind config if available (must be injected BEFORE CDN script)
446
+ const tailwindConfigScript = await ctx.loadTailwindConfigIfNeeded();
447
+
448
+ return `<!DOCTYPE html>
449
+ <html lang="en">
450
+ <head>
451
+ <meta charset="UTF-8">
452
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
453
+ <base href="${virtualPrefix}/">
454
+ <title>Next.js App</title>
455
+ ${envScript}
456
+ ${TAILWIND_CDN_SCRIPT}
457
+ ${tailwindConfigScript}
458
+ ${CORS_PROXY_SCRIPT}
459
+ ${globalCssLinks.join('\n ')}
460
+ <script type="importmap">
461
+ {
462
+ "imports": {
463
+ "react": "https://esm.sh/react@18.2.0?dev",
464
+ "react/": "https://esm.sh/react@18.2.0&dev/",
465
+ "react-dom": "https://esm.sh/react-dom@18.2.0?dev",
466
+ "react-dom/": "https://esm.sh/react-dom@18.2.0&dev/",
467
+ "react-dom/client": "https://esm.sh/react-dom@18.2.0/client?dev",
468
+ "next/link": "${virtualPrefix}/_next/shims/link.js",
469
+ "next/router": "${virtualPrefix}/_next/shims/router.js",
470
+ "next/head": "${virtualPrefix}/_next/shims/head.js",
471
+ "next/navigation": "${virtualPrefix}/_next/shims/navigation.js",
472
+ "next/image": "${virtualPrefix}/_next/shims/image.js",
473
+ "next/dynamic": "${virtualPrefix}/_next/shims/dynamic.js",
474
+ "next/script": "${virtualPrefix}/_next/shims/script.js",
475
+ "next/font/google": "${virtualPrefix}/_next/shims/font/google.js",
476
+ "next/font/local": "${virtualPrefix}/_next/shims/font/local.js"
477
+ }
478
+ }
479
+ </script>
480
+ ${REACT_REFRESH_PREAMBLE}
481
+ ${HMR_CLIENT_SCRIPT}
482
+ </head>
483
+ <body>
484
+ <div id="__next"></div>
485
+ <script type="module">
486
+ import React from 'react';
487
+ import ReactDOM from 'react-dom/client';
488
+
489
+ const virtualBase = '${virtualPrefix}';
490
+
491
+ // Convert URL path to page module path
492
+ function getPageModulePath(pathname) {
493
+ let route = pathname;
494
+ if (route.startsWith(virtualBase)) {
495
+ route = route.slice(virtualBase.length);
496
+ }
497
+ route = route.replace(/^\\/+/, '/') || '/';
498
+ const modulePath = route === '/' ? '/index' : route;
499
+ return virtualBase + '/_next/pages' + modulePath + '.js';
500
+ }
501
+
502
+ // Dynamic page loader
503
+ async function loadPage(pathname) {
504
+ const modulePath = getPageModulePath(pathname);
505
+ try {
506
+ const module = await import(/* @vite-ignore */ modulePath);
507
+ return module.default;
508
+ } catch (e) {
509
+ console.error('[Navigation] Failed to load:', modulePath, e);
510
+ return null;
511
+ }
512
+ }
513
+
514
+ // Router component
515
+ function Router() {
516
+ const [Page, setPage] = React.useState(null);
517
+ const [path, setPath] = React.useState(window.location.pathname);
518
+
519
+ React.useEffect(() => {
520
+ loadPage(path).then(C => C && setPage(() => C));
521
+ }, []);
522
+
523
+ React.useEffect(() => {
524
+ const handleNavigation = async () => {
525
+ const newPath = window.location.pathname;
526
+ if (newPath !== path) {
527
+ setPath(newPath);
528
+ const C = await loadPage(newPath);
529
+ if (C) setPage(() => C);
530
+ }
531
+ };
532
+ window.addEventListener('popstate', handleNavigation);
533
+ return () => window.removeEventListener('popstate', handleNavigation);
534
+ }, [path]);
535
+
536
+ if (!Page) return null;
537
+ return React.createElement(Page);
538
+ }
539
+
540
+ // Mark that we've initialized (for testing no-reload)
541
+ window.__NEXT_INITIALIZED__ = Date.now();
542
+
543
+ ReactDOM.createRoot(document.getElementById('__next')).render(
544
+ React.createElement(React.StrictMode, null, React.createElement(Router))
545
+ );
546
+ </script>
547
+ </body>
548
+ </html>`;
549
+ }
550
+
551
+ /**
552
+ * Serve a basic 404 page
553
+ */
554
+ export function serve404Page(port: number): ResponseData {
555
+ const virtualPrefix = `/__virtual__/${port}`;
556
+ const html = `<!DOCTYPE html>
557
+ <html lang="en">
558
+ <head>
559
+ <meta charset="UTF-8">
560
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
561
+ <base href="${virtualPrefix}/">
562
+ <title>404 - Page Not Found</title>
563
+ <style>
564
+ body {
565
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
566
+ display: flex;
567
+ flex-direction: column;
568
+ align-items: center;
569
+ justify-content: center;
570
+ min-height: 100vh;
571
+ margin: 0;
572
+ background: #fafafa;
573
+ }
574
+ h1 { font-size: 48px; margin: 0; }
575
+ p { color: #666; margin-top: 10px; }
576
+ a { color: #0070f3; text-decoration: none; }
577
+ a:hover { text-decoration: underline; }
578
+ </style>
579
+ </head>
580
+ <body>
581
+ <h1>404</h1>
582
+ <p>This page could not be found.</p>
583
+ <p><a href="/">Go back home</a></p>
584
+ </body>
585
+ </html>`;
586
+
587
+ const buffer = Buffer.from(html);
588
+ return {
589
+ statusCode: 404,
590
+ statusMessage: 'Not Found',
591
+ headers: {
592
+ 'Content-Type': 'text/html; charset=utf-8',
593
+ 'Content-Length': String(buffer.length),
594
+ },
595
+ body: buffer,
596
+ };
597
+ }