almostnode 0.2.7 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/__sw__.js +80 -84
- package/dist/assets/{runtime-worker-B8_LZkBX.js → runtime-worker-D8VYeuKv.js} +1448 -1121
- package/dist/assets/runtime-worker-D8VYeuKv.js.map +1 -0
- package/dist/frameworks/code-transforms.d.ts.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 +2895 -2454
- 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 +3208 -2782
- package/dist/index.mjs.map +1 -1
- package/dist/runtime.d.ts +20 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/server-bridge.d.ts +2 -0
- package/dist/server-bridge.d.ts.map +1 -1
- package/dist/shims/crypto.d.ts +2 -0
- package/dist/shims/crypto.d.ts.map +1 -1
- package/dist/shims/esbuild.d.ts.map +1 -1
- package/dist/shims/fs.d.ts.map +1 -1
- package/dist/shims/http.d.ts +29 -0
- package/dist/shims/http.d.ts.map +1 -1
- package/dist/shims/path.d.ts.map +1 -1
- package/dist/shims/stream.d.ts.map +1 -1
- package/dist/shims/vfs-adapter.d.ts.map +1 -1
- package/dist/shims/ws.d.ts +2 -0
- package/dist/shims/ws.d.ts.map +1 -1
- package/dist/utils/binary-encoding.d.ts +13 -0
- package/dist/utils/binary-encoding.d.ts.map +1 -0
- package/dist/virtual-fs.d.ts.map +1 -1
- package/package.json +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 +94 -15
- package/src/server-bridge.ts +61 -28
- package/src/shims/crypto.ts +13 -0
- package/src/shims/esbuild.ts +4 -1
- package/src/shims/fs.ts +9 -11
- package/src/shims/http.ts +309 -3
- package/src/shims/path.ts +6 -13
- package/src/shims/stream.ts +12 -26
- package/src/shims/vfs-adapter.ts +5 -2
- package/src/shims/ws.ts +92 -2
- package/src/utils/binary-encoding.ts +43 -0
- package/src/virtual-fs.ts +7 -15
- package/dist/assets/runtime-worker-B8_LZkBX.js.map +0 -1
|
@@ -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
|
+
}
|