@run0/jiki 0.1.0

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