@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,1252 @@
1
+ /**
2
+ * NextDevServer - Next.js-compatible dev server for the web-containers environment.
3
+ * Implements file-based routing, API routes, and HMR via postMessage.
4
+ *
5
+ * ## Framework Dev Server Roadmap
6
+ *
7
+ * Currently supported:
8
+ * - **Next.js** (Pages Router + App Router) — implemented in this file.
9
+ *
10
+ * Planned framework support:
11
+ * - **Vite SSR** — generic Vite-based dev server with SSR support.
12
+ * - **Remix** — loader/action-based routing with nested layouts.
13
+ * - **SvelteKit** — file-based routing with server-side load functions.
14
+ * - **Nuxt** — Vue-based file-system routing and server routes.
15
+ * - **Astro** — content-focused with island architecture.
16
+ * - **SolidStart** — SolidJS file-based routing with SSR.
17
+ *
18
+ * To add a new framework dev server, implement the {@link DevServer} base class
19
+ * from `../dev-server.ts`. The base class provides `handleRequest()` dispatch,
20
+ * static file serving, HMR event emission, and port management. The subclass
21
+ * must implement route resolution, code transformation, and HTML generation.
22
+ *
23
+ * ## Unsupported Next.js Features
24
+ *
25
+ * The following Next.js features are NOT supported in this browser-based runtime:
26
+ *
27
+ * - **Middleware** (`middleware.ts`): Edge runtime middleware is not executed.
28
+ * Requests go directly to route handlers without middleware interception.
29
+ *
30
+ * - **Server Actions**: React Server Actions (`"use server"` directive) are not
31
+ * supported. Form submissions using server actions will not work.
32
+ *
33
+ * - **React Server Components (RSC)**: All components run as client components.
34
+ * The `"use client"` directive is accepted but has no effect since everything
35
+ * is already client-side.
36
+ *
37
+ * - **Incremental Static Regeneration (ISR)**: `revalidate` options in
38
+ * `getStaticProps` or route segment configs are ignored. Pages are always
39
+ * rendered on demand.
40
+ *
41
+ * - **Internationalization (i18n)**: The `i18n` configuration in `next.config.js`
42
+ * is not processed. Locale detection and routing are not performed.
43
+ *
44
+ * - **Rewrites and Redirects**: Configuration-based rewrites and redirects from
45
+ * `next.config.js` are not applied. Use client-side routing instead.
46
+ *
47
+ * - **Parallel Routes and Intercepting Routes**: Advanced App Router patterns
48
+ * like `@slot` parallel routes and `(..)` intercepting routes are not supported.
49
+ *
50
+ * - **Image Optimization**: The `next/image` component renders a plain `<img>` tag.
51
+ * No server-side image optimization is performed.
52
+ *
53
+ * - **Edge Runtime**: API routes and pages cannot use `export const runtime = 'edge'`.
54
+ * All execution happens in the browser's main thread.
55
+ */
56
+
57
+ import {
58
+ DevServer,
59
+ type DevServerOptions,
60
+ type ResponseData,
61
+ type HMRUpdate,
62
+ } from "../dev-server";
63
+ import { MemFS } from "../memfs";
64
+ import { BufferImpl as Buffer } from "../polyfills/stream";
65
+ import { simpleHash } from "../utils/hash";
66
+ import { safePath } from "../utils/safe-path";
67
+ import {
68
+ redirectNpmImports as _redirectNpmImports,
69
+ stripCssImports as _stripCssImports,
70
+ addReactRefresh as _addReactRefresh,
71
+ transformEsmToCjsSimple,
72
+ type CssModuleContext,
73
+ } from "./code-transforms";
74
+ import {
75
+ NEXT_LINK_SHIM,
76
+ NEXT_ROUTER_SHIM,
77
+ NEXT_NAVIGATION_SHIM,
78
+ NEXT_HEAD_SHIM,
79
+ NEXT_IMAGE_SHIM,
80
+ NEXT_DYNAMIC_SHIM,
81
+ NEXT_SCRIPT_SHIM,
82
+ NEXT_FONT_GOOGLE_SHIM,
83
+ NEXT_FONT_LOCAL_SHIM,
84
+ } from "./next-shims";
85
+ import {
86
+ type AppRoute,
87
+ generateAppRouterHtml as _generateAppRouterHtml,
88
+ generatePageHtml as _generatePageHtml,
89
+ serve404Page as _serve404Page,
90
+ } from "./next-html-generator";
91
+ import {
92
+ type RouteResolverContext,
93
+ hasAppRouter,
94
+ resolveAppRoute,
95
+ resolveAppRouteHandler,
96
+ resolvePageFile,
97
+ resolveApiFile,
98
+ resolveFileWithExtension,
99
+ needsTransform,
100
+ } from "./next-route-resolver";
101
+ import {
102
+ createMockRequest,
103
+ createMockResponse,
104
+ createBuiltinModules,
105
+ executeApiHandler,
106
+ } from "./next-api-handler";
107
+ import { ESBUILD_WASM_ESM_CDN, ESBUILD_WASM_BINARY_CDN } from "../config/cdn";
108
+
109
+ const isBrowser =
110
+ typeof window !== "undefined" &&
111
+ typeof window.navigator !== "undefined" &&
112
+ "serviceWorker" in window.navigator;
113
+
114
+ declare global {
115
+ interface Window {
116
+ __esbuild?: typeof import("esbuild-wasm");
117
+ __esbuildInitPromise?: Promise<void>;
118
+ }
119
+ }
120
+
121
+ async function initEsbuild(): Promise<void> {
122
+ if (!isBrowser) return;
123
+ if (window.__esbuild) return;
124
+ if (window.__esbuildInitPromise) return window.__esbuildInitPromise;
125
+
126
+ window.__esbuildInitPromise = (async () => {
127
+ try {
128
+ const mod = await import(/* @vite-ignore */ ESBUILD_WASM_ESM_CDN);
129
+ const esbuildMod = mod.default || mod;
130
+
131
+ try {
132
+ await esbuildMod.initialize({ wasmURL: ESBUILD_WASM_BINARY_CDN });
133
+ } catch (initError) {
134
+ if (
135
+ initError instanceof Error &&
136
+ initError.message.includes('Cannot call "initialize" more than once')
137
+ ) {
138
+ // Already initialized, reuse
139
+ } else {
140
+ throw initError;
141
+ }
142
+ }
143
+
144
+ window.__esbuild = esbuildMod;
145
+ } catch (error) {
146
+ console.error("[NextDevServer] Failed to initialize esbuild:", error);
147
+ window.__esbuildInitPromise = undefined;
148
+ throw error;
149
+ }
150
+ })();
151
+
152
+ return window.__esbuildInitPromise;
153
+ }
154
+
155
+ function getEsbuild(): typeof import("esbuild-wasm") | undefined {
156
+ return isBrowser ? window.__esbuild : undefined;
157
+ }
158
+
159
+ export interface NextDevServerOptions extends DevServerOptions {
160
+ pagesDir?: string;
161
+ appDir?: string;
162
+ publicDir?: string;
163
+ preferAppRouter?: boolean;
164
+ env?: Record<string, string>;
165
+ basePath?: string;
166
+ additionalImportMap?: Record<string, string>;
167
+ additionalLocalPackages?: string[];
168
+ esmShDeps?: string;
169
+ /** Timeout in milliseconds for API handler execution. Defaults to 30000 (30s). */
170
+ apiHandlerTimeout?: number;
171
+ }
172
+
173
+ export class NextDevServer extends DevServer {
174
+ private pagesDir: string;
175
+ private appDir: string;
176
+ private publicDir: string;
177
+ private useAppRouter: boolean;
178
+ private watcherCleanup: (() => void) | null = null;
179
+ private hmrTargetWindow: Window | null = null;
180
+ private options: NextDevServerOptions;
181
+ private transformCache: Map<string, { code: string; hash: string }> =
182
+ new Map();
183
+ private pathAliases: Map<string, string> = new Map();
184
+ private basePath: string = "";
185
+
186
+ private get routeCtx(): RouteResolverContext {
187
+ return {
188
+ exists: (path: string) => this.exists(path),
189
+ isDirectory: (path: string) => this.isDirectory(path),
190
+ readdir: (path: string) => this.vfs.readdirSync(path) as string[],
191
+ };
192
+ }
193
+
194
+ constructor(vfs: MemFS, options: NextDevServerOptions) {
195
+ super(vfs, options);
196
+ this.options = options;
197
+
198
+ this.pagesDir = options.pagesDir || "/pages";
199
+ this.appDir = options.appDir || "/app";
200
+ this.publicDir = options.publicDir || "/public";
201
+
202
+ if (options.preferAppRouter !== undefined) {
203
+ this.useAppRouter = options.preferAppRouter;
204
+ } else {
205
+ this.useAppRouter = hasAppRouter(this.appDir, this.routeCtx);
206
+ }
207
+
208
+ this.loadPathAliases();
209
+ this.basePath = options.basePath || "";
210
+ }
211
+
212
+ private loadPathAliases(): void {
213
+ try {
214
+ const tsconfigPath = "/tsconfig.json";
215
+ if (!this.vfs.existsSync(tsconfigPath)) return;
216
+
217
+ const content = this.vfs.readFileSync(tsconfigPath, "utf-8") as string;
218
+ const tsconfig = JSON.parse(content);
219
+ const paths = tsconfig?.compilerOptions?.paths;
220
+ if (!paths) return;
221
+
222
+ for (const [alias, targets] of Object.entries(paths)) {
223
+ if (Array.isArray(targets) && targets.length > 0) {
224
+ const aliasPrefix = alias.replace(/\*$/, "");
225
+ const targetPrefix = (targets[0] as string)
226
+ .replace(/\*$/, "")
227
+ .replace(/^\./, "");
228
+ this.pathAliases.set(aliasPrefix, targetPrefix);
229
+ }
230
+ }
231
+ } catch {
232
+ /* ignore */
233
+ }
234
+ }
235
+
236
+ private resolvePathAliases(code: string, currentFile: string): string {
237
+ if (this.pathAliases.size === 0) return code;
238
+
239
+ const virtualBase = `/__virtual__/${this.port}`;
240
+ let result = code;
241
+
242
+ for (const [alias, target] of this.pathAliases) {
243
+ const aliasEscaped = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
244
+ const pattern = new RegExp(
245
+ `(from\\s*['"]|import\\s*\\(\\s*['"])${aliasEscaped}([^'"]+)(['"])`,
246
+ "g",
247
+ );
248
+
249
+ result = result.replace(pattern, (match, prefix, path, quote) => {
250
+ const resolvedPath = `${virtualBase}${target}${path}`;
251
+ return `${prefix}${resolvedPath}${quote}`;
252
+ });
253
+ }
254
+
255
+ return result;
256
+ }
257
+
258
+ setEnv(key: string, value: string): void {
259
+ this.options.env = this.options.env || {};
260
+ this.options.env[key] = value;
261
+ }
262
+
263
+ getEnv(): Record<string, string> {
264
+ return { ...this.options.env };
265
+ }
266
+
267
+ /**
268
+ * Set the target window for HMR updates (typically iframe.contentWindow).
269
+ * Enables postMessage-based HMR delivery to sandboxed iframes.
270
+ */
271
+ setHMRTarget(targetWindow: Window): void {
272
+ this.hmrTargetWindow = targetWindow;
273
+ }
274
+
275
+ private generateEnvScript(): string {
276
+ const env = this.options.env || {};
277
+ const publicEnvVars: Record<string, string> = {};
278
+ for (const [key, value] of Object.entries(env)) {
279
+ if (key.startsWith("NEXT_PUBLIC_")) {
280
+ publicEnvVars[key] = value;
281
+ }
282
+ }
283
+
284
+ return `<script>
285
+ window.process = window.process || {};
286
+ window.process.env = window.process.env || {};
287
+ Object.assign(window.process.env, ${JSON.stringify(publicEnvVars)});
288
+ window.__NEXT_BASE_PATH__ = ${JSON.stringify(this.basePath)};
289
+ </script>`;
290
+ }
291
+
292
+ async handleRequest(
293
+ method: string,
294
+ url: string,
295
+ headers: Record<string, string>,
296
+ body?: Buffer,
297
+ ): Promise<ResponseData> {
298
+ const urlObj = new URL(url, "http://localhost");
299
+ let pathname = urlObj.pathname;
300
+
301
+ const virtualPrefixMatch = pathname.match(/^\/__virtual__\/\d+/);
302
+ if (virtualPrefixMatch) {
303
+ pathname = pathname.slice(virtualPrefixMatch[0].length) || "/";
304
+ }
305
+
306
+ if (this.basePath && pathname.startsWith(this.basePath)) {
307
+ const rest = pathname.slice(this.basePath.length);
308
+ if (rest === "" || rest.startsWith("/")) {
309
+ pathname = rest || "/";
310
+ }
311
+ }
312
+
313
+ if (pathname.startsWith("/_next/shims/"))
314
+ return this.serveNextShim(pathname);
315
+ if (pathname === "/_next/route-info")
316
+ return this.serveRouteInfo(urlObj.searchParams.get("pathname") || "/");
317
+ if (pathname.startsWith("/_next/pages/"))
318
+ return this.servePageComponent(pathname);
319
+ if (pathname.startsWith("/_next/app/"))
320
+ return this.serveAppComponent(pathname);
321
+
322
+ if (pathname.startsWith("/_npm/"))
323
+ return this.serveNpmModule(pathname.slice(5));
324
+
325
+ if (this.useAppRouter) {
326
+ const appRouteFile = resolveAppRouteHandler(
327
+ this.appDir,
328
+ pathname,
329
+ this.routeCtx,
330
+ );
331
+ if (appRouteFile) {
332
+ return this.handleAppRouteHandler(
333
+ method,
334
+ pathname,
335
+ headers,
336
+ body,
337
+ appRouteFile,
338
+ urlObj.search,
339
+ );
340
+ }
341
+ }
342
+
343
+ if (pathname.startsWith("/api/")) {
344
+ return this.handleApiRoute(method, pathname, headers, body);
345
+ }
346
+
347
+ const publicPath = this.publicDir + pathname;
348
+ if (this.exists(publicPath) && !this.isDirectory(publicPath)) {
349
+ return this.serveFile(publicPath);
350
+ }
351
+
352
+ if (needsTransform(pathname) && this.exists(pathname)) {
353
+ return this.transformAndServe(pathname, pathname);
354
+ }
355
+
356
+ const resolvedFile = resolveFileWithExtension(pathname, this.routeCtx);
357
+ if (resolvedFile) {
358
+ if (needsTransform(resolvedFile))
359
+ return this.transformAndServe(resolvedFile, pathname);
360
+ return this.serveFile(resolvedFile);
361
+ }
362
+
363
+ if (this.exists(pathname) && !this.isDirectory(pathname)) {
364
+ return this.serveFile(pathname);
365
+ }
366
+
367
+ return this.handlePageRoute(pathname, urlObj.search);
368
+ }
369
+
370
+ private serveNextShim(pathname: string): ResponseData {
371
+ const shimName = pathname.replace("/_next/shims/", "").replace(".js", "");
372
+
373
+ const SHIM_MAP: Record<string, string> = {
374
+ link: NEXT_LINK_SHIM,
375
+ router: NEXT_ROUTER_SHIM,
376
+ head: NEXT_HEAD_SHIM,
377
+ navigation: NEXT_NAVIGATION_SHIM,
378
+ image: NEXT_IMAGE_SHIM,
379
+ dynamic: NEXT_DYNAMIC_SHIM,
380
+ script: NEXT_SCRIPT_SHIM,
381
+ "font/google": NEXT_FONT_GOOGLE_SHIM,
382
+ "font/local": NEXT_FONT_LOCAL_SHIM,
383
+ };
384
+
385
+ const code = SHIM_MAP[shimName];
386
+ if (!code) return this.notFound(pathname);
387
+
388
+ const buffer = Buffer.from(code);
389
+ return {
390
+ statusCode: 200,
391
+ statusMessage: "OK",
392
+ headers: {
393
+ "Content-Type": "application/javascript; charset=utf-8",
394
+ "Content-Length": String(buffer.length),
395
+ "Cache-Control": "no-cache",
396
+ },
397
+ body: buffer,
398
+ };
399
+ }
400
+
401
+ private serveRouteInfo(pathname: string): ResponseData {
402
+ const route = resolveAppRoute(this.appDir, pathname, this.routeCtx);
403
+ const info = route
404
+ ? {
405
+ params: route.params,
406
+ found: true,
407
+ page: route.page,
408
+ layouts: route.layouts,
409
+ }
410
+ : { params: {}, found: false };
411
+
412
+ const json = JSON.stringify(info);
413
+ const buffer = Buffer.from(json);
414
+
415
+ return {
416
+ statusCode: 200,
417
+ statusMessage: "OK",
418
+ headers: {
419
+ "Content-Type": "application/json; charset=utf-8",
420
+ "Content-Length": String(buffer.length),
421
+ "Cache-Control": "no-cache",
422
+ },
423
+ body: buffer,
424
+ };
425
+ }
426
+
427
+ private async servePageComponent(pathname: string): Promise<ResponseData> {
428
+ const route = pathname.replace("/_next/pages", "").replace(/\.js$/, "");
429
+ const pageFile = resolvePageFile(this.pagesDir, route, this.routeCtx);
430
+ if (!pageFile) return this.notFound(pathname);
431
+ return this.transformAndServe(pageFile, pageFile);
432
+ }
433
+
434
+ private async serveAppComponent(pathname: string): Promise<ResponseData> {
435
+ const rawFilePath = pathname.replace("/_next/app", "");
436
+
437
+ if (this.exists(rawFilePath) && !this.isDirectory(rawFilePath)) {
438
+ return this.transformAndServe(rawFilePath, rawFilePath);
439
+ }
440
+
441
+ const filePath = rawFilePath.replace(/\.js$/, "");
442
+ const extensions = [".tsx", ".jsx", ".ts", ".js"];
443
+ for (const ext of extensions) {
444
+ const fullPath = filePath + ext;
445
+ if (this.exists(fullPath))
446
+ return this.transformAndServe(fullPath, fullPath);
447
+ }
448
+
449
+ return this.notFound(pathname);
450
+ }
451
+
452
+ private async handleApiRoute(
453
+ method: string,
454
+ pathname: string,
455
+ headers: Record<string, string>,
456
+ body?: Buffer,
457
+ ): Promise<ResponseData> {
458
+ const apiFile = resolveApiFile(this.pagesDir, pathname, this.routeCtx);
459
+ if (!apiFile) {
460
+ return {
461
+ statusCode: 404,
462
+ statusMessage: "Not Found",
463
+ headers: { "Content-Type": "application/json; charset=utf-8" },
464
+ body: Buffer.from(JSON.stringify({ error: "API route not found" })),
465
+ };
466
+ }
467
+
468
+ try {
469
+ const code = this.vfs.readFileSync(apiFile, "utf8") as string;
470
+ const transformed = await this.transformApiHandler(code, apiFile);
471
+ const req = createMockRequest(method, pathname, headers, body);
472
+ const res = createMockResponse();
473
+
474
+ const builtins = await createBuiltinModules();
475
+ await executeApiHandler(
476
+ transformed,
477
+ req,
478
+ res,
479
+ this.options.env,
480
+ builtins,
481
+ );
482
+
483
+ if (!res.isEnded()) {
484
+ const timeoutMs = this.options.apiHandlerTimeout ?? 30000;
485
+ const timeout = new Promise<void>((_, reject) => {
486
+ setTimeout(
487
+ () => reject(new Error(`API handler timeout after ${timeoutMs}ms`)),
488
+ timeoutMs,
489
+ );
490
+ });
491
+ await Promise.race([res.waitForEnd(), timeout]);
492
+ }
493
+
494
+ return res.toResponse();
495
+ } catch (error) {
496
+ console.error("[NextDevServer] API error:", error);
497
+ return {
498
+ statusCode: 500,
499
+ statusMessage: "Internal Server Error",
500
+ headers: { "Content-Type": "application/json; charset=utf-8" },
501
+ body: Buffer.from(
502
+ JSON.stringify({
503
+ error:
504
+ error instanceof Error ? error.message : "Internal Server Error",
505
+ }),
506
+ ),
507
+ };
508
+ }
509
+ }
510
+
511
+ private async handleAppRouteHandler(
512
+ method: string,
513
+ pathname: string,
514
+ headers: Record<string, string>,
515
+ body: Buffer | undefined,
516
+ routeFile: string,
517
+ search?: string,
518
+ ): Promise<ResponseData> {
519
+ try {
520
+ const code = this.vfs.readFileSync(routeFile, "utf8") as string;
521
+ const transformed = await this.transformApiHandler(code, routeFile);
522
+
523
+ const builtinModules = await createBuiltinModules();
524
+ const require = (id: string): unknown => {
525
+ const modId = id.startsWith("node:") ? id.slice(5) : id;
526
+ if ((builtinModules as Record<string, unknown>)[modId])
527
+ return (builtinModules as Record<string, unknown>)[modId];
528
+ throw new Error(`Module not found: ${id}`);
529
+ };
530
+
531
+ const moduleObj = { exports: {} as Record<string, unknown> };
532
+ const exports = moduleObj.exports;
533
+ const process = {
534
+ env: { ...this.options.env },
535
+ cwd: () => "/",
536
+ platform: "browser",
537
+ version: "v18.0.0",
538
+ versions: { node: "18.0.0" },
539
+ };
540
+
541
+ const fn = new Function(
542
+ "exports",
543
+ "require",
544
+ "module",
545
+ "process",
546
+ transformed,
547
+ );
548
+ fn(exports, require, moduleObj, process);
549
+
550
+ const methodUpper = method.toUpperCase();
551
+ const handler =
552
+ moduleObj.exports[methodUpper] ||
553
+ moduleObj.exports[methodUpper.toLowerCase()];
554
+
555
+ if (typeof handler !== "function") {
556
+ return {
557
+ statusCode: 405,
558
+ statusMessage: "Method Not Allowed",
559
+ headers: { "Content-Type": "application/json; charset=utf-8" },
560
+ body: Buffer.from(
561
+ JSON.stringify({ error: `Method ${method} not allowed` }),
562
+ ),
563
+ };
564
+ }
565
+
566
+ const requestUrl = new URL(pathname + (search || ""), "http://localhost");
567
+ const requestInit: RequestInit = {
568
+ method: methodUpper,
569
+ headers: new Headers(headers),
570
+ };
571
+ if (body && methodUpper !== "GET" && methodUpper !== "HEAD") {
572
+ requestInit.body = body;
573
+ }
574
+ const request = new Request(requestUrl.toString(), requestInit);
575
+ const route = resolveAppRoute(this.appDir, pathname, this.routeCtx);
576
+ const params = route?.params || {};
577
+
578
+ const response = await handler(request, {
579
+ params: Promise.resolve(params),
580
+ });
581
+
582
+ if (response instanceof Response) {
583
+ const respHeaders: Record<string, string> = {};
584
+ response.headers.forEach((value: string, key: string) => {
585
+ respHeaders[key] = value;
586
+ });
587
+ const respBody = await response.text();
588
+ return {
589
+ statusCode: response.status,
590
+ statusMessage: response.statusText || "OK",
591
+ headers: respHeaders,
592
+ body: Buffer.from(respBody),
593
+ };
594
+ }
595
+
596
+ if (response && typeof response === "object") {
597
+ const json = JSON.stringify(response);
598
+ return {
599
+ statusCode: 200,
600
+ statusMessage: "OK",
601
+ headers: { "Content-Type": "application/json; charset=utf-8" },
602
+ body: Buffer.from(json),
603
+ };
604
+ }
605
+
606
+ return {
607
+ statusCode: 200,
608
+ statusMessage: "OK",
609
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
610
+ body: Buffer.from(String(response || "")),
611
+ };
612
+ } catch (error) {
613
+ console.error("[NextDevServer] App Route handler error:", error);
614
+ return {
615
+ statusCode: 500,
616
+ statusMessage: "Internal Server Error",
617
+ headers: { "Content-Type": "application/json; charset=utf-8" },
618
+ body: Buffer.from(
619
+ JSON.stringify({
620
+ error:
621
+ error instanceof Error ? error.message : "Internal Server Error",
622
+ }),
623
+ ),
624
+ };
625
+ }
626
+ }
627
+
628
+ private async handlePageRoute(
629
+ pathname: string,
630
+ search: string,
631
+ ): Promise<ResponseData> {
632
+ if (this.useAppRouter) return this.handleAppRouterPage(pathname, search);
633
+
634
+ const pageFile = resolvePageFile(this.pagesDir, pathname, this.routeCtx);
635
+ if (!pageFile) {
636
+ const notFoundPage = resolvePageFile(
637
+ this.pagesDir,
638
+ "/404",
639
+ this.routeCtx,
640
+ );
641
+ if (notFoundPage) {
642
+ const html = await this.generatePageHtml(notFoundPage, "/404");
643
+ return {
644
+ statusCode: 404,
645
+ statusMessage: "Not Found",
646
+ headers: { "Content-Type": "text/html; charset=utf-8" },
647
+ body: Buffer.from(html),
648
+ };
649
+ }
650
+ return this.serve404Page();
651
+ }
652
+
653
+ if (needsTransform(pathname))
654
+ return this.transformAndServe(pageFile, pathname);
655
+
656
+ const html = await this.generatePageHtml(pageFile, pathname);
657
+ const buffer = Buffer.from(html);
658
+ return {
659
+ statusCode: 200,
660
+ statusMessage: "OK",
661
+ headers: {
662
+ "Content-Type": "text/html; charset=utf-8",
663
+ "Content-Length": String(buffer.length),
664
+ "Cache-Control": "no-cache",
665
+ },
666
+ body: buffer,
667
+ };
668
+ }
669
+
670
+ private async handleAppRouterPage(
671
+ pathname: string,
672
+ _search: string,
673
+ ): Promise<ResponseData> {
674
+ const route = resolveAppRoute(this.appDir, pathname, this.routeCtx);
675
+
676
+ if (!route) {
677
+ const notFoundRoute = resolveAppRoute(
678
+ this.appDir,
679
+ "/not-found",
680
+ this.routeCtx,
681
+ );
682
+ if (notFoundRoute) {
683
+ const html = await this.generateAppRouterHtml(
684
+ notFoundRoute,
685
+ "/not-found",
686
+ );
687
+ return {
688
+ statusCode: 404,
689
+ statusMessage: "Not Found",
690
+ headers: { "Content-Type": "text/html; charset=utf-8" },
691
+ body: Buffer.from(html),
692
+ };
693
+ }
694
+ return this.serve404Page();
695
+ }
696
+
697
+ const html = await this.generateAppRouterHtml(route, pathname);
698
+ const buffer = Buffer.from(html);
699
+ return {
700
+ statusCode: 200,
701
+ statusMessage: "OK",
702
+ headers: {
703
+ "Content-Type": "text/html; charset=utf-8",
704
+ "Content-Length": String(buffer.length),
705
+ "Cache-Control": "no-cache",
706
+ },
707
+ body: buffer,
708
+ };
709
+ }
710
+
711
+ private _tailwindConfigCache: string | undefined;
712
+
713
+ private async loadTailwindConfig(): Promise<string> {
714
+ if (this._tailwindConfigCache !== undefined)
715
+ return this._tailwindConfigCache;
716
+
717
+ const configFiles = [
718
+ "/tailwind.config.ts",
719
+ "/tailwind.config.js",
720
+ "/tailwind.config.mjs",
721
+ ];
722
+
723
+ let content: string | null = null;
724
+ for (const file of configFiles) {
725
+ const full = this.root === "/" ? file : `${this.root}${file}`;
726
+ try {
727
+ const raw = this.vfs.readFileSync(full, "utf-8");
728
+ content =
729
+ typeof raw === "string"
730
+ ? raw
731
+ : new TextDecoder().decode(raw as Uint8Array);
732
+ break;
733
+ } catch {
734
+ /* try next */
735
+ }
736
+ }
737
+
738
+ if (!content) {
739
+ this._tailwindConfigCache = "";
740
+ return "";
741
+ }
742
+
743
+ try {
744
+ let js = content;
745
+ js = js.replace(
746
+ /import\s+type\s+\{[^}]*\}\s+from\s+['"][^'"]*['"]\s*;?\s*/g,
747
+ "",
748
+ );
749
+ js = js.replace(
750
+ /import\s+\{[^}]*\}\s+from\s+['"][^'"]*['"]\s*;?\s*/g,
751
+ "",
752
+ );
753
+ js = js.replace(/\s+satisfies\s+\w+\s*;?\s*$/gm, "");
754
+ js = js.replace(/:\s*[A-Z]\w*\s*=/g, " =");
755
+ js = js.replace(/\s+as\s+const\s*/g, " ");
756
+
757
+ const expMatch = js.match(/export\s+default\s*/);
758
+ if (!expMatch || expMatch.index === undefined) {
759
+ this._tailwindConfigCache = "";
760
+ return "";
761
+ }
762
+
763
+ const after = js
764
+ .substring(expMatch.index + expMatch[0].length)
765
+ .trimStart();
766
+ if (!after.startsWith("{")) {
767
+ this._tailwindConfigCache = "";
768
+ return "";
769
+ }
770
+
771
+ const objStart =
772
+ expMatch.index +
773
+ expMatch[0].length +
774
+ (js.substring(expMatch.index + expMatch[0].length).length -
775
+ after.length);
776
+ const objContent = js.substring(objStart);
777
+
778
+ let braces = 0,
779
+ inStr = false,
780
+ strCh = "",
781
+ esc = false,
782
+ endIdx = -1;
783
+ for (let i = 0; i < objContent.length; i++) {
784
+ const c = objContent[i];
785
+ if (esc) {
786
+ esc = false;
787
+ continue;
788
+ }
789
+ if (c === "\\") {
790
+ esc = true;
791
+ continue;
792
+ }
793
+ if (inStr) {
794
+ if (c === strCh) inStr = false;
795
+ continue;
796
+ }
797
+ if (c === '"' || c === "'" || c === "`") {
798
+ inStr = true;
799
+ strCh = c;
800
+ continue;
801
+ }
802
+ if (c === "{") braces++;
803
+ else if (c === "}") {
804
+ braces--;
805
+ if (braces === 0) {
806
+ endIdx = i + 1;
807
+ break;
808
+ }
809
+ }
810
+ }
811
+
812
+ if (endIdx === -1) {
813
+ this._tailwindConfigCache = "";
814
+ return "";
815
+ }
816
+
817
+ const configObj = objContent.substring(0, endIdx);
818
+ this._tailwindConfigCache = `<script>\n tailwind.config = ${configObj};\n</script>`;
819
+ } catch {
820
+ this._tailwindConfigCache = "";
821
+ }
822
+
823
+ return this._tailwindConfigCache;
824
+ }
825
+
826
+ private htmlContext() {
827
+ return {
828
+ port: this.port,
829
+ exists: (path: string) => this.exists(path),
830
+ generateEnvScript: () => this.generateEnvScript(),
831
+ loadTailwindConfigIfNeeded: () => this.loadTailwindConfig(),
832
+ additionalImportMap: this.options.additionalImportMap,
833
+ };
834
+ }
835
+
836
+ private async generateAppRouterHtml(
837
+ route: AppRoute,
838
+ pathname: string,
839
+ ): Promise<string> {
840
+ return _generateAppRouterHtml(this.htmlContext(), route, pathname);
841
+ }
842
+
843
+ private async generatePageHtml(
844
+ pageFile: string,
845
+ pathname: string,
846
+ ): Promise<string> {
847
+ return _generatePageHtml(this.htmlContext(), pageFile, pathname);
848
+ }
849
+
850
+ private serve404Page(): ResponseData {
851
+ return _serve404Page(this.port);
852
+ }
853
+
854
+ private async transformAndServe(
855
+ filePath: string,
856
+ urlPath: string,
857
+ ): Promise<ResponseData> {
858
+ try {
859
+ const content = this.vfs.readFileSync(filePath, "utf8") as string;
860
+ const hash = simpleHash(content);
861
+
862
+ const cached = this.transformCache.get(filePath);
863
+ if (cached && cached.hash === hash) {
864
+ const buffer = Buffer.from(cached.code);
865
+ return {
866
+ statusCode: 200,
867
+ statusMessage: "OK",
868
+ headers: {
869
+ "Content-Type": "application/javascript; charset=utf-8",
870
+ "Content-Length": String(buffer.length),
871
+ "Cache-Control": "no-cache",
872
+ "X-Transformed": "true",
873
+ "X-Cache": "hit",
874
+ },
875
+ body: buffer,
876
+ };
877
+ }
878
+
879
+ const transformed = await this.transformCode(content, filePath);
880
+
881
+ this.transformCache.set(filePath, { code: transformed, hash });
882
+ if (this.transformCache.size > 500) {
883
+ const firstKey = this.transformCache.keys().next().value;
884
+ if (firstKey) this.transformCache.delete(firstKey);
885
+ }
886
+
887
+ const buffer = Buffer.from(transformed);
888
+ return {
889
+ statusCode: 200,
890
+ statusMessage: "OK",
891
+ headers: {
892
+ "Content-Type": "application/javascript; charset=utf-8",
893
+ "Content-Length": String(buffer.length),
894
+ "Cache-Control": "no-cache",
895
+ "X-Transformed": "true",
896
+ },
897
+ body: buffer,
898
+ };
899
+ } catch (error) {
900
+ console.error("[NextDevServer] Transform error:", error);
901
+ const message =
902
+ error instanceof Error ? error.message : "Transform failed";
903
+ const errorJs = `// Transform Error: ${message}\nconsole.error(${JSON.stringify(
904
+ message,
905
+ )});`;
906
+ return {
907
+ statusCode: 200,
908
+ statusMessage: "OK",
909
+ headers: {
910
+ "Content-Type": "application/javascript; charset=utf-8",
911
+ "X-Transform-Error": "true",
912
+ },
913
+ body: Buffer.from(errorJs),
914
+ };
915
+ }
916
+ }
917
+
918
+ private async transformCode(code: string, filename: string): Promise<string> {
919
+ if (!isBrowser) {
920
+ return this.doStripCssImports(code, filename);
921
+ }
922
+
923
+ await initEsbuild();
924
+ const esbuild = getEsbuild();
925
+ if (!esbuild) throw new Error("esbuild not available");
926
+
927
+ const codeWithoutCss = this.doStripCssImports(code, filename);
928
+ const codeWithAliases = this.resolvePathAliases(codeWithoutCss, filename);
929
+
930
+ let loader: "js" | "jsx" | "ts" | "tsx" = "js";
931
+ if (filename.endsWith(".jsx")) loader = "jsx";
932
+ else if (filename.endsWith(".tsx")) loader = "tsx";
933
+ else if (filename.endsWith(".ts")) loader = "ts";
934
+
935
+ const result = await esbuild.transform(codeWithAliases, {
936
+ loader,
937
+ format: "esm",
938
+ target: "esnext",
939
+ jsx: "automatic",
940
+ jsxImportSource: "react",
941
+ sourcemap: "inline",
942
+ sourcefile: filename,
943
+ });
944
+
945
+ const codeWithCdnImports = this.doRedirectNpmImports(result.code);
946
+
947
+ if (/\.(jsx|tsx)$/.test(filename)) {
948
+ return _addReactRefresh(codeWithCdnImports, filename);
949
+ }
950
+
951
+ return codeWithCdnImports;
952
+ }
953
+
954
+ private _dependencies: Record<string, string> | undefined;
955
+
956
+ private getDependencies(): Record<string, string> {
957
+ if (this._dependencies) return this._dependencies;
958
+ let deps: Record<string, string> = {};
959
+ try {
960
+ const pkgPath = `${this.root}/package.json`;
961
+ if (this.vfs.existsSync(pkgPath)) {
962
+ const pkg = JSON.parse(
963
+ this.vfs.readFileSync(pkgPath, "utf-8") as string,
964
+ );
965
+ deps = { ...pkg.dependencies, ...pkg.devDependencies };
966
+ }
967
+ } catch {
968
+ /* ignore */
969
+ }
970
+ this._dependencies = deps;
971
+ return deps;
972
+ }
973
+
974
+ private _installedPackages: Set<string> | undefined;
975
+
976
+ private getInstalledPackages(): Set<string> {
977
+ if (this._installedPackages) return this._installedPackages;
978
+ const installed = new Set<string>();
979
+ try {
980
+ const nmDir = `${this.root === "/" ? "" : this.root}/node_modules`;
981
+ if (this.vfs.existsSync(nmDir)) {
982
+ const entries = this.vfs.readdirSync(nmDir) as string[];
983
+ for (const entry of entries) {
984
+ if (entry.startsWith(".")) continue;
985
+ if (entry.startsWith("@")) {
986
+ try {
987
+ const scopeDir = `${nmDir}/${entry}`;
988
+ const scoped = this.vfs.readdirSync(scopeDir) as string[];
989
+ for (const s of scoped) installed.add(`${entry}/${s}`);
990
+ } catch {
991
+ /* ignore */
992
+ }
993
+ } else {
994
+ installed.add(entry);
995
+ }
996
+ }
997
+ }
998
+ } catch {
999
+ /* ignore */
1000
+ }
1001
+ this._installedPackages = installed;
1002
+ return installed;
1003
+ }
1004
+
1005
+ invalidatePackageCache(): void {
1006
+ this._dependencies = undefined;
1007
+ this._installedPackages = undefined;
1008
+ }
1009
+
1010
+ private doRedirectNpmImports(code: string): string {
1011
+ return _redirectNpmImports(
1012
+ code,
1013
+ this.options.additionalLocalPackages,
1014
+ this.getDependencies(),
1015
+ this.options.esmShDeps,
1016
+ this.getInstalledPackages(),
1017
+ );
1018
+ }
1019
+
1020
+ private doStripCssImports(code: string, currentFile?: string): string {
1021
+ return _stripCssImports(code, currentFile, this.getCssModuleContext());
1022
+ }
1023
+
1024
+ private getCssModuleContext(): CssModuleContext {
1025
+ return {
1026
+ readFile: (path: string) =>
1027
+ this.vfs.readFileSync(path, "utf-8") as string,
1028
+ exists: (path: string) => this.exists(path),
1029
+ };
1030
+ }
1031
+
1032
+ private async transformApiHandler(
1033
+ code: string,
1034
+ filename: string,
1035
+ ): Promise<string> {
1036
+ const codeWithAliases = this.resolvePathAliases(code, filename);
1037
+
1038
+ if (isBrowser) {
1039
+ await initEsbuild();
1040
+ const esbuild = getEsbuild();
1041
+ if (!esbuild) throw new Error("esbuild not available");
1042
+
1043
+ let loader: "js" | "jsx" | "ts" | "tsx" = "js";
1044
+ if (filename.endsWith(".jsx")) loader = "jsx";
1045
+ else if (filename.endsWith(".tsx")) loader = "tsx";
1046
+ else if (filename.endsWith(".ts")) loader = "ts";
1047
+
1048
+ const result = await esbuild.transform(codeWithAliases, {
1049
+ loader,
1050
+ format: "cjs",
1051
+ target: "esnext",
1052
+ platform: "neutral",
1053
+ sourcefile: filename,
1054
+ });
1055
+
1056
+ return result.code;
1057
+ }
1058
+
1059
+ return transformEsmToCjsSimple(codeWithAliases);
1060
+ }
1061
+
1062
+ protected serveFile(filePath: string): ResponseData {
1063
+ if (filePath.endsWith(".json")) {
1064
+ try {
1065
+ const normalizedPath = this.resolvePath(filePath);
1066
+ const content = this.vfs.readFileSync(normalizedPath);
1067
+ let jsonContent: string;
1068
+ if (typeof content === "string") {
1069
+ jsonContent = content;
1070
+ } else if (content instanceof Uint8Array) {
1071
+ jsonContent = new TextDecoder("utf-8").decode(content);
1072
+ } else {
1073
+ jsonContent = Buffer.from(content).toString("utf-8");
1074
+ }
1075
+
1076
+ const esModuleContent = `export default ${jsonContent};`;
1077
+ const buffer = Buffer.from(esModuleContent);
1078
+
1079
+ return {
1080
+ statusCode: 200,
1081
+ statusMessage: "OK",
1082
+ headers: {
1083
+ "Content-Type": "application/javascript; charset=utf-8",
1084
+ "Content-Length": String(buffer.length),
1085
+ "Cache-Control": "no-cache",
1086
+ },
1087
+ body: buffer,
1088
+ };
1089
+ } catch (error) {
1090
+ if ((error as NodeJS.ErrnoException).code === "ENOENT")
1091
+ return this.notFound(filePath);
1092
+ return this.serverError(error);
1093
+ }
1094
+ }
1095
+
1096
+ return super.serveFile(filePath);
1097
+ }
1098
+
1099
+ /**
1100
+ * Serve an npm module from /node_modules/ with ESM-to-CJS transform.
1101
+ * Accessed via /_npm/{package}/{file} URLs from the import map.
1102
+ */
1103
+ private serveNpmModule(modulePath: string): ResponseData {
1104
+ const fsPath = safePath("/node_modules", "/" + modulePath);
1105
+
1106
+ if (!modulePath || modulePath === "/") {
1107
+ return this.notFound(fsPath);
1108
+ }
1109
+
1110
+ let source: string;
1111
+ try {
1112
+ source = this.vfs.readFileSync(fsPath, "utf-8") as string;
1113
+ } catch {
1114
+ return this.notFound(fsPath);
1115
+ }
1116
+
1117
+ if (fsPath.endsWith(".json")) {
1118
+ const js = `export default ${source};`;
1119
+ const buf = Buffer.from(js);
1120
+ return {
1121
+ statusCode: 200,
1122
+ statusMessage: "OK",
1123
+ headers: {
1124
+ "Content-Type": "application/javascript; charset=utf-8",
1125
+ "Content-Length": String(buf.length),
1126
+ "Cache-Control": "no-cache",
1127
+ },
1128
+ body: buf,
1129
+ };
1130
+ }
1131
+
1132
+ let transformed = source;
1133
+ try {
1134
+ transformed = transformEsmToCjsSimple(source);
1135
+ } catch {
1136
+ /* serve original on transform failure */
1137
+ }
1138
+
1139
+ const buf = Buffer.from(transformed);
1140
+ return {
1141
+ statusCode: 200,
1142
+ statusMessage: "OK",
1143
+ headers: {
1144
+ "Content-Type": "application/javascript; charset=utf-8",
1145
+ "Content-Length": String(buf.length),
1146
+ "Cache-Control": "no-cache",
1147
+ },
1148
+ body: buf,
1149
+ };
1150
+ }
1151
+
1152
+ /**
1153
+ * Start file watching for HMR.
1154
+ * Watches /pages, /app, and /public for changes and emits HMR updates.
1155
+ */
1156
+ startWatching(): void {
1157
+ const watchers: Array<{ close: () => void }> = [];
1158
+
1159
+ try {
1160
+ const pagesWatcher = this.vfs.watch(
1161
+ this.pagesDir,
1162
+ { recursive: true },
1163
+ (eventType, filename) => {
1164
+ if (eventType === "change" && filename) {
1165
+ const fullPath = filename.startsWith("/")
1166
+ ? filename
1167
+ : `${this.pagesDir}/${filename}`;
1168
+ this.handleFileChange(fullPath);
1169
+ }
1170
+ },
1171
+ );
1172
+ watchers.push(pagesWatcher);
1173
+ } catch {
1174
+ /* pages dir may not exist */
1175
+ }
1176
+
1177
+ if (this.useAppRouter) {
1178
+ try {
1179
+ const appWatcher = this.vfs.watch(
1180
+ this.appDir,
1181
+ { recursive: true },
1182
+ (eventType, filename) => {
1183
+ if (eventType === "change" && filename) {
1184
+ const fullPath = filename.startsWith("/")
1185
+ ? filename
1186
+ : `${this.appDir}/${filename}`;
1187
+ this.handleFileChange(fullPath);
1188
+ }
1189
+ },
1190
+ );
1191
+ watchers.push(appWatcher);
1192
+ } catch {
1193
+ /* app dir may not exist */
1194
+ }
1195
+ }
1196
+
1197
+ try {
1198
+ const publicWatcher = this.vfs.watch(
1199
+ this.publicDir,
1200
+ { recursive: true },
1201
+ (eventType, filename) => {
1202
+ if (eventType === "change" && filename) {
1203
+ this.handleFileChange(`${this.publicDir}/${filename}`);
1204
+ }
1205
+ },
1206
+ );
1207
+ watchers.push(publicWatcher);
1208
+ } catch {
1209
+ /* public dir may not exist */
1210
+ }
1211
+
1212
+ this.watcherCleanup = () => {
1213
+ watchers.forEach(w => w.close());
1214
+ };
1215
+ }
1216
+
1217
+ private handleFileChange(path: string): void {
1218
+ const isCSS = path.endsWith(".css");
1219
+ const isJS = /\.(jsx?|tsx?)$/.test(path);
1220
+ const updateType = isCSS || isJS ? "update" : "full-reload";
1221
+
1222
+ this.transformCache.delete(path);
1223
+
1224
+ if (path.includes("tailwind.config")) this._tailwindConfigCache = undefined;
1225
+ if (path.includes("node_modules")) this._installedPackages = undefined;
1226
+
1227
+ const update: HMRUpdate = { type: updateType, path, timestamp: Date.now() };
1228
+ this.emitHMRUpdate(update);
1229
+
1230
+ if (this.hmrTargetWindow) {
1231
+ try {
1232
+ this.hmrTargetWindow.postMessage(
1233
+ { ...update, channel: "next-hmr" },
1234
+ "*",
1235
+ );
1236
+ } catch {
1237
+ /* window may be closed */
1238
+ }
1239
+ }
1240
+ }
1241
+
1242
+ stop(): void {
1243
+ if (this.watcherCleanup) {
1244
+ this.watcherCleanup();
1245
+ this.watcherCleanup = null;
1246
+ }
1247
+ this.hmrTargetWindow = null;
1248
+ super.stop();
1249
+ }
1250
+ }
1251
+
1252
+ export default NextDevServer;