From 506d82567dbacd61e4f4d3f66f1d3cbbddd845d7 Mon Sep 17 00:00:00 2001 From: MD YUNUS <115855149+yunus25jmi1@users.noreply.github.com> Date: Sun, 1 Mar 2026 19:21:00 +0000 Subject: [PATCH 01/14] fix: serve static HTML files from public/ after rewrites (issue #199) Address code review feedback: - Extract shared MIME type map (server/mime.ts) eliminating 3x duplication - Add path traversal guard using resolve + startsWith pattern - Use path.extname() instead of string splitting - Use path.join/resolve in generated RSC entry instead of string concat - Clean up navigation/headers context before returning static response - Add nested route test (public/auth/no-access.html via rewrite) --- packages/vinext/src/index.ts | 42 +++++++++++++-- packages/vinext/src/server/app-dev-server.ts | 51 +++++++++++++++++++ packages/vinext/src/server/mime.ts | 46 +++++++++++++++++ packages/vinext/src/server/prod-server.ts | 44 ++++++++-------- tests/app-router.test.ts | 37 ++++++++++++++ tests/fixtures/app-basic/next.config.ts | 4 ++ .../app-basic/public/auth/no-access.html | 5 ++ .../app-basic/public/static-html-page.html | 5 ++ tests/fixtures/pages-basic/next.config.mjs | 10 ++++ .../pages-basic/public/auth/no-access.html | 5 ++ .../pages-basic/public/static-html-page.html | 5 ++ tests/pages-router.test.ts | 21 ++++++++ 12 files changed, 248 insertions(+), 27 deletions(-) create mode 100644 packages/vinext/src/server/mime.ts create mode 100644 tests/fixtures/app-basic/public/auth/no-access.html create mode 100644 tests/fixtures/app-basic/public/static-html-page.html create mode 100644 tests/fixtures/pages-basic/public/auth/no-access.html create mode 100644 tests/fixtures/pages-basic/public/static-html-page.html diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 710c0d13e..ee0d45c33 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -40,6 +40,7 @@ import { scanMetadataFiles } from "./server/metadata-routes.js"; import { staticExportPages } from "./build/static-export.js"; import { detectPackageManager } from "./utils/project.js"; import { asyncHooksStubPlugin } from "./plugins/async-hooks-stub.js"; +import { mimeType } from "./server/mime.js"; import tsconfigPaths from "vite-tsconfig-paths"; import react, { Options as VitePluginReactOptions } from "@vitejs/plugin-react"; import MagicString from "magic-string"; @@ -2343,7 +2344,7 @@ hydrate(); headers: nextConfig?.headers, allowedOrigins: nextConfig?.serverActionsAllowedOrigins, allowedDevOrigins: nextConfig?.allowedDevOrigins, - }, instrumentationPath); + }, instrumentationPath, root); } if (id === RESOLVED_APP_SSR_ENTRY && hasAppDir) { return generateSsrEntry(); @@ -2969,10 +2970,32 @@ hydrate(); nextConfig.rewrites.afterFiles, reqCtx, ); - if (afterRewrite) resolvedUrl = afterRewrite; + if (afterRewrite) { + // External rewrite from afterFiles — proxy to external URL + if (isExternalUrl(afterRewrite)) { + await proxyExternalRewriteNode(req, res, afterRewrite); + return; + } + resolvedUrl = afterRewrite; + // If the rewritten path has a file extension, it may point to a + // static file in public/. Serve it directly before route matching + // (which would miss it and SSR would return 404). + const afterFilesPathname = afterRewrite.split("?")[0]; + if (path.extname(afterFilesPathname)) { + const resolvedPublicDir = path.resolve(root, "public"); + const publicFilePath = path.resolve(resolvedPublicDir, "." + afterFilesPathname); + if (publicFilePath.startsWith(resolvedPublicDir + path.sep) && fs.existsSync(publicFilePath) && fs.statSync(publicFilePath).isFile()) { + const content = fs.readFileSync(publicFilePath); + const ext = (path.extname(afterFilesPathname).slice(1)).toLowerCase(); + res.writeHead(200, { "Content-Type": mimeType(ext) }); + res.end(content); + return; + } + } + } } - // External rewrite from afterFiles — proxy to external URL + // External rewrite (from beforeFiles) — proxy to external URL if (isExternalUrl(resolvedUrl)) { await proxyExternalRewriteNode(req, res, resolvedUrl); return; @@ -3001,6 +3024,19 @@ hydrate(); await proxyExternalRewriteNode(req, res, fallbackRewrite); return; } + // Check if fallback targets a static file in public/ + const fallbackPathname = fallbackRewrite.split("?")[0]; + if (path.extname(fallbackPathname)) { + const resolvedPublicDir = path.resolve(root, "public"); + const publicFilePath = path.resolve(resolvedPublicDir, "." + fallbackPathname); + if (publicFilePath.startsWith(resolvedPublicDir + path.sep) && fs.existsSync(publicFilePath) && fs.statSync(publicFilePath).isFile()) { + const content = fs.readFileSync(publicFilePath); + const ext = (path.extname(fallbackPathname).slice(1)).toLowerCase(); + res.writeHead(200, { "Content-Type": mimeType(ext) }); + res.end(content); + return; + } + } await handler(req, res, fallbackRewrite, mwStatus); return; } diff --git a/packages/vinext/src/server/app-dev-server.ts b/packages/vinext/src/server/app-dev-server.ts index 66bd2ec83..4d72d0c44 100644 --- a/packages/vinext/src/server/app-dev-server.ts +++ b/packages/vinext/src/server/app-dev-server.ts @@ -7,6 +7,7 @@ * the SSR entry for HTML generation. */ import fs from "node:fs"; +import path from "node:path"; import { fileURLToPath } from "node:url"; import type { AppRoute } from "../routing/app-router.js"; import type { MetadataFileRoute } from "./metadata-routes.js"; @@ -14,6 +15,7 @@ import type { NextRedirect, NextRewrite, NextHeader } from "../config/next-confi import { generateDevOriginCheckCode } from "./dev-origin-check.js"; import { generateSafeRegExpCode, generateMiddlewareMatcherCode, generateNormalizePathCode } from "./middleware-codegen.js"; import { isProxyFile } from "./middleware.js"; +import { MIME_TYPES } from "./mime.js"; /** * Resolved config options relevant to App Router request handling. @@ -50,6 +52,7 @@ export function generateRscEntry( trailingSlash?: boolean, config?: AppRouterConfig, instrumentationPath?: string | null, + root?: string, ): string { const bp = basePath ?? ""; const ts = trailingSlash ?? false; @@ -57,6 +60,14 @@ export function generateRscEntry( const rewrites = config?.rewrites ?? { beforeFiles: [], afterFiles: [], fallback: [] }; const headers = config?.headers ?? []; const allowedOrigins = config?.allowedOrigins ?? []; + // Compute the public/ directory path for serving static files after rewrites. + // appDir is something like /project/app or /project/src/app; root is the Vite root. + // We require `root` for correctness — path.dirname(appDir) is wrong for src/app layouts + // (e.g. /project/src/public instead of /project/public). + if (!root) { + console.warn("[vinext] generateRscEntry: root not provided, static file serving after rewrites will be disabled"); + } + const publicDir = root ? path.join(root, "public") : null; // Build import map for all page and layout files const imports: string[] = []; const importMap: Map = new Map(); @@ -207,6 +218,8 @@ ${slotEntries.join(",\n")} }); return ` +import __nodeFs from "node:fs"; +import __nodePath from "node:path"; import { renderToReadableStream, decodeReply, @@ -980,6 +993,7 @@ const __configRedirects = ${JSON.stringify(redirects)}; const __configRewrites = ${JSON.stringify(rewrites)}; const __configHeaders = ${JSON.stringify(headers)}; const __allowedOrigins = ${JSON.stringify(allowedOrigins)}; +const __mimeTypes = ${JSON.stringify(MIME_TYPES)}; ${generateDevOriginCheckCode(config?.allowedDevOrigins)} @@ -1805,6 +1819,25 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { return __proxyExternalRequest(request, __afterRewritten); } cleanPathname = __afterRewritten; + // If the rewritten path has a file extension, it may point to a static + // file in public/. Serve it directly before route matching. + const __afterExtname = __nodePath.extname(cleanPathname); + if (__afterExtname && ${JSON.stringify(publicDir)} !== null) { + const __afterPublicRoot = ${JSON.stringify(publicDir)}; + const __afterPublicFile = __nodePath.resolve(__afterPublicRoot, "." + cleanPathname); + if (__afterPublicFile.startsWith(__afterPublicRoot + __nodePath.sep)) { + try { + const __afterStat = __nodeFs.statSync(__afterPublicFile); + if (__afterStat.isFile()) { + const __afterContent = __nodeFs.readFileSync(__afterPublicFile); + const __afterExt = __afterExtname.slice(1).toLowerCase(); + setHeadersContext(null); + setNavigationContext(null); + return new Response(__afterContent, { status: 200, headers: { "Content-Type": __mimeTypes[__afterExt] ?? "application/octet-stream" } }); + } + } catch { /* file doesn't exist or not readable */ } + } + } } } @@ -1820,6 +1853,24 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { return __proxyExternalRequest(request, __fallbackRewritten); } cleanPathname = __fallbackRewritten; + // Check if fallback targets a static file in public/ + const __fbExtname = __nodePath.extname(cleanPathname); + if (__fbExtname && ${JSON.stringify(publicDir)} !== null) { + const __fbPublicRoot = ${JSON.stringify(publicDir)}; + const __fbPublicFile = __nodePath.resolve(__fbPublicRoot, "." + cleanPathname); + if (__fbPublicFile.startsWith(__fbPublicRoot + __nodePath.sep)) { + try { + const __fbStat = __nodeFs.statSync(__fbPublicFile); + if (__fbStat.isFile()) { + const __fbContent = __nodeFs.readFileSync(__fbPublicFile); + const __fbExt = __fbExtname.slice(1).toLowerCase(); + setHeadersContext(null); + setNavigationContext(null); + return new Response(__fbContent, { status: 200, headers: { "Content-Type": __mimeTypes[__fbExt] ?? "application/octet-stream" } }); + } + } catch { /* file doesn't exist or not readable */ } + } + } match = matchRoute(cleanPathname, routes); } } diff --git a/packages/vinext/src/server/mime.ts b/packages/vinext/src/server/mime.ts new file mode 100644 index 000000000..5f29f74ee --- /dev/null +++ b/packages/vinext/src/server/mime.ts @@ -0,0 +1,46 @@ +/** + * Shared MIME type map for serving static files. + * + * Used by index.ts (Pages Router dev), app-dev-server.ts (generated RSC entry), + * and prod-server.ts (production server). Centralised here to avoid drift. + * + * Keys are bare extensions (no leading dot). Use `mimeType()` for lookup. + */ +export const MIME_TYPES: Record = { + html: "text/html; charset=utf-8", + htm: "text/html; charset=utf-8", + css: "text/css", + js: "application/javascript", + mjs: "application/javascript", + cjs: "application/javascript", + json: "application/json", + txt: "text/plain", + xml: "application/xml", + svg: "image/svg+xml", + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + webp: "image/webp", + avif: "image/avif", + ico: "image/x-icon", + woff: "font/woff", + woff2: "font/woff2", + ttf: "font/ttf", + otf: "font/otf", + eot: "application/vnd.ms-fontobject", + pdf: "application/pdf", + map: "application/json", + mp4: "video/mp4", + webm: "video/webm", + mp3: "audio/mpeg", + ogg: "audio/ogg", +}; + +/** + * Look up a MIME type by bare extension (no leading dot). + * Returns "application/octet-stream" for unknown extensions. + */ +export function mimeType(ext: string): string { + return MIME_TYPES[ext] ?? "application/octet-stream"; +} diff --git a/packages/vinext/src/server/prod-server.ts b/packages/vinext/src/server/prod-server.ts index afc5a2ddc..b6d792519 100644 --- a/packages/vinext/src/server/prod-server.ts +++ b/packages/vinext/src/server/prod-server.ts @@ -28,6 +28,7 @@ import type { RequestContext } from "../config/config-matchers.js"; import { IMAGE_OPTIMIZATION_PATH, IMAGE_CONTENT_SECURITY_POLICY, parseImageParams, isSafeImageContentType, DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES, type ImageConfig } from "./image-optimization.js"; import { normalizePath } from "./normalize-path.js"; import { computeLazyChunks } from "../index.js"; +import { mimeType } from "./mime.js"; /** Convert a Node.js IncomingMessage into a ReadableStream for Web Request body. */ function readNodeStream(req: IncomingMessage): ReadableStream { @@ -184,27 +185,11 @@ function sendCompressed( } } -/** Content-type lookup for static assets. */ -const CONTENT_TYPES: Record = { - ".js": "application/javascript", - ".mjs": "application/javascript", - ".css": "text/css", - ".html": "text/html", - ".json": "application/json", - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".svg": "image/svg+xml", - ".ico": "image/x-icon", - ".woff": "font/woff", - ".woff2": "font/woff2", - ".ttf": "font/ttf", - ".eot": "application/vnd.ms-fontobject", - ".webp": "image/webp", - ".avif": "image/avif", - ".map": "application/json", -}; +/** Content-type lookup for static assets using the shared MIME map. */ +function contentType(ext: string): string { + // ext comes from path.extname() so it has a leading dot (e.g. ".js") + return mimeType(ext.startsWith(".") ? ext.slice(1) : ext); +} /** * Try to serve a static file from the client build directory. @@ -247,7 +232,7 @@ function tryServeStatic( } const ext = path.extname(staticFile); - const ct = CONTENT_TYPES[ext] ?? "application/octet-stream"; + const ct = contentType(ext); const isHashed = pathname.startsWith("/assets/"); const cacheControl = isHashed ? "public, max-age=31536000, immutable" @@ -566,7 +551,7 @@ async function startAppRouterServer(options: AppRouterServerOptions) { // Block SVG and other unsafe content types by checking the file extension. // SVG is only allowed when dangerouslyAllowSVG is enabled in next.config.js. const ext = path.extname(params.imageUrl).toLowerCase(); - const ct = CONTENT_TYPES[ext] ?? "application/octet-stream"; + const ct = contentType(ext); if (!isSafeImageContentType(ct, imageConfig?.dangerouslyAllowSVG)) { res.writeHead(400); res.end("The requested resource is not an allowed image type"); @@ -733,7 +718,7 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { // Block SVG and other unsafe content types. // SVG is only allowed when dangerouslyAllowSVG is enabled. const ext = path.extname(params.imageUrl).toLowerCase(); - const ct = CONTENT_TYPES[ext] ?? "application/octet-stream"; + const ct = contentType(ext); if (!isSafeImageContentType(ct, pagesImageConfig?.dangerouslyAllowSVG)) { res.writeHead(400); res.end("The requested resource is not an allowed image type"); @@ -967,6 +952,12 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { } resolvedUrl = rewritten; resolvedPathname = rewritten.split("?")[0]; + // If the rewritten path has a file extension, it may point to a static + // file in public/ (copied to clientDir during build). Try to serve it + // directly before falling through to SSR (which would return 404). + if (path.extname(resolvedPathname) && tryServeStatic(req, res, clientDir, resolvedPathname, compress)) { + return; + } } } @@ -984,6 +975,11 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { await sendWebResponse(proxyResponse, req, res, compress); return; } + // Check if fallback targets a static file in public/ + const fallbackPathname = fallbackRewrite.split("?")[0]; + if (path.extname(fallbackPathname) && tryServeStatic(req, res, clientDir, fallbackPathname, compress)) { + return; + } response = await renderPage(webRequest, fallbackRewrite, ssrManifest); } } diff --git a/tests/app-router.test.ts b/tests/app-router.test.ts index 55f1d7cf9..ca2b9bc6a 100644 --- a/tests/app-router.test.ts +++ b/tests/app-router.test.ts @@ -1997,6 +1997,27 @@ describe("App Router next.config.js features (dev server integration)", () => { expect(html).toContain("About"); }); + it("serves static HTML file from public/ when afterFiles rewrite points to .html path", async () => { + // Regresses issue #199: async rewrites() returning flat array (→ afterFiles) that maps + // a clean URL to a .html file in public/ should serve the file, not return 404. + const res = await fetch(`${baseUrl}/static-html-page`); + expect(res.status).toBe(200); + const html = await res.text(); + expect(html).toContain("Hello from static HTML"); + // Should be served with text/html content-type + expect(res.headers.get("content-type")).toMatch(/text\/html/i); + }); + + it("serves nested static HTML file from public/ subdirectory via rewrite", async () => { + // Nested rewrites: /auth/no-access → /auth/no-access.html should resolve + // to public/auth/no-access.html and serve it. + const res = await fetch(`${baseUrl}/auth/no-access`); + expect(res.status).toBe(200); + const html = await res.text(); + expect(html).toContain("Access denied from nested static HTML"); + expect(res.headers.get("content-type")).toMatch(/text\/html/i); + }); + it("applies custom headers from next.config.js on API routes", async () => { const res = await fetch(`${baseUrl}/api/hello`); expect(res.headers.get("x-custom-header")).toBe("vinext-app"); @@ -2128,6 +2149,22 @@ describe("App Router next.config.js features (generateRscEntry)", () => { expect(code).toContain("/fallback-rewrite"); }); + it("embeds root/public path for serving static files after rewrite", () => { + // When root is provided, the generated code should contain that public path + // so it can serve .html files from public/ when a rewrite produces a .html path. + const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {}, null, "/tmp/test"); + // path.join uses OS separators; the generated code embeds via JSON.stringify + const expectedPublicDir = path.join("/tmp/test", "public"); + expect(code).toContain(JSON.stringify(expectedPublicDir)); + // Should contain the node:fs and node:path imports for the static file handler + expect(code).toContain("__nodeFs"); + expect(code).toContain("__nodePath"); + expect(code).toContain("statSync"); + // Should use path.resolve + startsWith for traversal protection + expect(code).toContain("__nodePath.resolve"); + expect(code).toContain("startsWith"); + }); + it("generates custom header handling code when headers are provided", () => { const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, { headers: [ diff --git a/tests/fixtures/app-basic/next.config.ts b/tests/fixtures/app-basic/next.config.ts index 0613b746f..1fed9f2db 100644 --- a/tests/fixtures/app-basic/next.config.ts +++ b/tests/fixtures/app-basic/next.config.ts @@ -87,6 +87,10 @@ const nextConfig: NextConfig = { { source: "/after-rewrite-about", destination: "/about" }, // Used by E2E: config-redirect.spec.ts { source: "/config-rewrite", destination: "/" }, + // Used by Vitest: app-router.test.ts (rewrite to static HTML in public/) + { source: "/static-html-page", destination: "/static-html-page.html" }, + // Used by Vitest: app-router.test.ts (nested rewrite to static HTML in public/) + { source: "/auth/no-access", destination: "/auth/no-access.html" }, ], fallback: [ // Used by Vitest: app-router.test.ts — fallback rewrite gated on a diff --git a/tests/fixtures/app-basic/public/auth/no-access.html b/tests/fixtures/app-basic/public/auth/no-access.html new file mode 100644 index 000000000..1f7745666 --- /dev/null +++ b/tests/fixtures/app-basic/public/auth/no-access.html @@ -0,0 +1,5 @@ + + +No Access +Access denied from nested static HTML + diff --git a/tests/fixtures/app-basic/public/static-html-page.html b/tests/fixtures/app-basic/public/static-html-page.html new file mode 100644 index 000000000..36a9bc316 --- /dev/null +++ b/tests/fixtures/app-basic/public/static-html-page.html @@ -0,0 +1,5 @@ + + +Static HTML Page +

Hello from static HTML

+ diff --git a/tests/fixtures/pages-basic/next.config.mjs b/tests/fixtures/pages-basic/next.config.mjs index 2cbd727a8..f20c53ae8 100644 --- a/tests/fixtures/pages-basic/next.config.mjs +++ b/tests/fixtures/pages-basic/next.config.mjs @@ -43,6 +43,16 @@ const nextConfig = { has: [{ type: "cookie", key: "mw-user" }], destination: "/about", }, + // Used by Vitest: pages-router.test.ts (rewrite to static HTML in public/) + { + source: "/static-html-page", + destination: "/static-html-page.html", + }, + // Used by Vitest: pages-router.test.ts (nested rewrite to static HTML in public/) + { + source: "/auth/no-access", + destination: "/auth/no-access.html", + }, ], fallback: [ { diff --git a/tests/fixtures/pages-basic/public/auth/no-access.html b/tests/fixtures/pages-basic/public/auth/no-access.html new file mode 100644 index 000000000..1f7745666 --- /dev/null +++ b/tests/fixtures/pages-basic/public/auth/no-access.html @@ -0,0 +1,5 @@ + + +No Access +Access denied from nested static HTML + diff --git a/tests/fixtures/pages-basic/public/static-html-page.html b/tests/fixtures/pages-basic/public/static-html-page.html new file mode 100644 index 000000000..36a9bc316 --- /dev/null +++ b/tests/fixtures/pages-basic/public/static-html-page.html @@ -0,0 +1,5 @@ + + +Static HTML Page +

Hello from static HTML

+ diff --git a/tests/pages-router.test.ts b/tests/pages-router.test.ts index 57cb2e712..1b0890799 100644 --- a/tests/pages-router.test.ts +++ b/tests/pages-router.test.ts @@ -287,6 +287,27 @@ describe("Pages Router integration", () => { expect(html).toContain("About"); }); + it("serves static HTML file from public/ when afterFiles rewrite points to .html path", async () => { + // Regresses issue #199: rewrites (afterFiles) that map a clean URL to a .html file + // in public/ should serve the file, not return 404. + const res = await fetch(`${baseUrl}/static-html-page`); + expect(res.status).toBe(200); + const html = await res.text(); + expect(html).toContain("Hello from static HTML"); + // Should be served with text/html content-type + expect(res.headers.get("content-type")).toMatch(/text\/html/i); + }); + + it("serves nested static HTML file from public/ subdirectory via rewrite", async () => { + // Nested rewrites: /auth/no-access → /auth/no-access.html should resolve + // to public/auth/no-access.html and serve it. + const res = await fetch(`${baseUrl}/auth/no-access`); + expect(res.status).toBe(200); + const html = await res.text(); + expect(html).toContain("Access denied from nested static HTML"); + expect(res.headers.get("content-type")).toMatch(/text\/html/i); + }); + it("applies fallback rewrites from next.config.js", async () => { const res = await fetch(`${baseUrl}/fallback-rewrite`); expect(res.status).toBe(200); From cc56d2c88376eada0a0ac22b8a194e5a6b6d97e3 Mon Sep 17 00:00:00 2001 From: MD YUNUS Date: Mon, 9 Mar 2026 01:37:40 +0530 Subject: [PATCH 02/14] address bonk review: hoist resolvedPublicDir, log non-ENOENT errors, add wasm MIME type, document leading-slash assumption --- packages/vinext/src/index.ts | 6 ++++-- packages/vinext/src/server/app-dev-server.ts | 7 +++++-- packages/vinext/src/server/mime.ts | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index fd52582bf..41e0811e2 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -3066,6 +3066,8 @@ hydrate(); const routes = await pagesRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher); + const resolvedPublicDir = path.resolve(root, "public"); + // Apply afterFiles rewrites — these run after initial route matching // If beforeFiles already rewrote the URL, afterFiles still run on the // *resolved* pathname. Next.js applies these when route matching succeeds @@ -3088,7 +3090,7 @@ hydrate(); // (which would miss it and SSR would return 404). const afterFilesPathname = afterRewrite.split("?")[0]; if (path.extname(afterFilesPathname)) { - const resolvedPublicDir = path.resolve(root, "public"); + // "." + afterFilesPathname works because rewrite destinations always start with "/" const publicFilePath = path.resolve(resolvedPublicDir, "." + afterFilesPathname); if (publicFilePath.startsWith(resolvedPublicDir + path.sep) && fs.existsSync(publicFilePath) && fs.statSync(publicFilePath).isFile()) { const content = fs.readFileSync(publicFilePath); @@ -3133,7 +3135,7 @@ hydrate(); // Check if fallback targets a static file in public/ const fallbackPathname = fallbackRewrite.split("?")[0]; if (path.extname(fallbackPathname)) { - const resolvedPublicDir = path.resolve(root, "public"); + // "." + fallbackPathname: see afterFiles comment above — leading "/" is assumed const publicFilePath = path.resolve(resolvedPublicDir, "." + fallbackPathname); if (publicFilePath.startsWith(resolvedPublicDir + path.sep) && fs.existsSync(publicFilePath) && fs.statSync(publicFilePath).isFile()) { const content = fs.readFileSync(publicFilePath); diff --git a/packages/vinext/src/server/app-dev-server.ts b/packages/vinext/src/server/app-dev-server.ts index ac41f1e5e..0ba22dd58 100644 --- a/packages/vinext/src/server/app-dev-server.ts +++ b/packages/vinext/src/server/app-dev-server.ts @@ -1920,6 +1920,8 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { const __afterExtname = __nodePath.extname(cleanPathname); if (__afterExtname && ${JSON.stringify(publicDir)} !== null) { const __afterPublicRoot = ${JSON.stringify(publicDir)}; + // "." + cleanPathname works because rewrite destinations always start with "/"; + // the traversal guard below catches any malformed path regardless. const __afterPublicFile = __nodePath.resolve(__afterPublicRoot, "." + cleanPathname); if (__afterPublicFile.startsWith(__afterPublicRoot + __nodePath.sep)) { try { @@ -1931,7 +1933,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { setNavigationContext(null); return new Response(__afterContent, { status: 200, headers: { "Content-Type": __mimeTypes[__afterExt] ?? "application/octet-stream" } }); } - } catch { /* file doesn't exist or not readable */ } + } catch (__e) { if (__e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', __e); } } } } @@ -1953,6 +1955,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { const __fbExtname = __nodePath.extname(cleanPathname); if (__fbExtname && ${JSON.stringify(publicDir)} !== null) { const __fbPublicRoot = ${JSON.stringify(publicDir)}; + // "." + cleanPathname: see afterFiles comment above — leading "/" is assumed. const __fbPublicFile = __nodePath.resolve(__fbPublicRoot, "." + cleanPathname); if (__fbPublicFile.startsWith(__fbPublicRoot + __nodePath.sep)) { try { @@ -1964,7 +1967,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { setNavigationContext(null); return new Response(__fbContent, { status: 200, headers: { "Content-Type": __mimeTypes[__fbExt] ?? "application/octet-stream" } }); } - } catch { /* file doesn't exist or not readable */ } + } catch (__e) { if (__e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', __e); } } } match = matchRoute(cleanPathname, routes); diff --git a/packages/vinext/src/server/mime.ts b/packages/vinext/src/server/mime.ts index 5f29f74ee..25a98e3e3 100644 --- a/packages/vinext/src/server/mime.ts +++ b/packages/vinext/src/server/mime.ts @@ -35,6 +35,7 @@ export const MIME_TYPES: Record = { webm: "video/webm", mp3: "audio/mpeg", ogg: "audio/ogg", + wasm: "application/wasm", }; /** From c4835eedf9b9eaa0128edbdb6c06872e9a6fa2ad Mon Sep 17 00:00:00 2001 From: MD YUNUS Date: Mon, 9 Mar 2026 02:18:51 +0530 Subject: [PATCH 03/14] address bonk review: hoist __publicDir to module scope, add fallback rewrite static file tests --- packages/vinext/src/server/app-dev-server.ts | 20 ++++++++----------- tests/app-router.test.ts | 12 +++++++++++ tests/fixtures/app-basic/next.config.ts | 2 ++ .../app-basic/public/fallback-page.html | 5 +++++ tests/fixtures/pages-basic/next.config.mjs | 5 +++++ .../pages-basic/public/fallback-page.html | 5 +++++ tests/pages-router.test.ts | 10 ++++++++++ 7 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 tests/fixtures/app-basic/public/fallback-page.html create mode 100644 tests/fixtures/pages-basic/public/fallback-page.html diff --git a/packages/vinext/src/server/app-dev-server.ts b/packages/vinext/src/server/app-dev-server.ts index 0ba22dd58..e48dfed95 100644 --- a/packages/vinext/src/server/app-dev-server.ts +++ b/packages/vinext/src/server/app-dev-server.ts @@ -65,11 +65,8 @@ export function generateRscEntry( const bodySizeLimit = config?.bodySizeLimit ?? 1 * 1024 * 1024; // Compute the public/ directory path for serving static files after rewrites. // appDir is something like /project/app or /project/src/app; root is the Vite root. - // We require `root` for correctness — path.dirname(appDir) is wrong for src/app layouts + // We need `root` for correctness — path.dirname(appDir) is wrong for src/app layouts // (e.g. /project/src/public instead of /project/public). - if (!root) { - console.warn("[vinext] generateRscEntry: root not provided, static file serving after rewrites will be disabled"); - } const publicDir = root ? path.join(root, "public") : null; // Build import map for all page and layout files const imports: string[] = []; @@ -1080,6 +1077,7 @@ const __configRewrites = ${JSON.stringify(rewrites)}; const __configHeaders = ${JSON.stringify(headers)}; const __allowedOrigins = ${JSON.stringify(allowedOrigins)}; const __mimeTypes = ${JSON.stringify(MIME_TYPES)}; +const __publicDir = ${JSON.stringify(publicDir)}; ${generateDevOriginCheckCode(config?.allowedDevOrigins)} @@ -1918,12 +1916,11 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { // If the rewritten path has a file extension, it may point to a static // file in public/. Serve it directly before route matching. const __afterExtname = __nodePath.extname(cleanPathname); - if (__afterExtname && ${JSON.stringify(publicDir)} !== null) { - const __afterPublicRoot = ${JSON.stringify(publicDir)}; + if (__afterExtname && __publicDir !== null) { // "." + cleanPathname works because rewrite destinations always start with "/"; // the traversal guard below catches any malformed path regardless. - const __afterPublicFile = __nodePath.resolve(__afterPublicRoot, "." + cleanPathname); - if (__afterPublicFile.startsWith(__afterPublicRoot + __nodePath.sep)) { + const __afterPublicFile = __nodePath.resolve(__publicDir, "." + cleanPathname); + if (__afterPublicFile.startsWith(__publicDir + __nodePath.sep)) { try { const __afterStat = __nodeFs.statSync(__afterPublicFile); if (__afterStat.isFile()) { @@ -1953,11 +1950,10 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { cleanPathname = __fallbackRewritten; // Check if fallback targets a static file in public/ const __fbExtname = __nodePath.extname(cleanPathname); - if (__fbExtname && ${JSON.stringify(publicDir)} !== null) { - const __fbPublicRoot = ${JSON.stringify(publicDir)}; + if (__fbExtname && __publicDir !== null) { // "." + cleanPathname: see afterFiles comment above — leading "/" is assumed. - const __fbPublicFile = __nodePath.resolve(__fbPublicRoot, "." + cleanPathname); - if (__fbPublicFile.startsWith(__fbPublicRoot + __nodePath.sep)) { + const __fbPublicFile = __nodePath.resolve(__publicDir, "." + cleanPathname); + if (__fbPublicFile.startsWith(__publicDir + __nodePath.sep)) { try { const __fbStat = __nodeFs.statSync(__fbPublicFile); if (__fbStat.isFile()) { diff --git a/tests/app-router.test.ts b/tests/app-router.test.ts index c52c24dfc..d75b50507 100644 --- a/tests/app-router.test.ts +++ b/tests/app-router.test.ts @@ -2045,6 +2045,16 @@ describe("App Router next.config.js features (dev server integration)", () => { expect(res.headers.get("content-type")).toMatch(/text\/html/i); }); + it("serves static HTML file from public/ when fallback rewrite points to .html path", async () => { + // Fallback rewrites run after route matching fails. /fallback-static-page has no + // matching app route, so the fallback rewrite maps it to /fallback-page.html in public/. + const res = await fetch(`${baseUrl}/fallback-static-page`); + expect(res.status).toBe(200); + const html = await res.text(); + expect(html).toContain("Hello from fallback static HTML"); + expect(res.headers.get("content-type")).toMatch(/text\/html/i); + }); + it("applies custom headers from next.config.js on API routes", async () => { const res = await fetch(`${baseUrl}/api/hello`); expect(res.headers.get("x-custom-header")).toBe("vinext-app"); @@ -2183,6 +2193,8 @@ describe("App Router next.config.js features (generateRscEntry)", () => { // path.join uses OS separators; the generated code embeds via JSON.stringify const expectedPublicDir = path.join("/tmp/test", "public"); expect(code).toContain(JSON.stringify(expectedPublicDir)); + // __publicDir should be hoisted to module scope + expect(code).toContain("const __publicDir = " + JSON.stringify(expectedPublicDir)); // Should contain the node:fs and node:path imports for the static file handler expect(code).toContain("__nodeFs"); expect(code).toContain("__nodePath"); diff --git a/tests/fixtures/app-basic/next.config.ts b/tests/fixtures/app-basic/next.config.ts index 1fed9f2db..bc67e8591 100644 --- a/tests/fixtures/app-basic/next.config.ts +++ b/tests/fixtures/app-basic/next.config.ts @@ -102,6 +102,8 @@ const nextConfig: NextConfig = { has: [{ type: "cookie", key: "mw-fallback-user" }], destination: "/about", }, + // Used by Vitest: app-router.test.ts (fallback rewrite to static HTML in public/) + { source: "/fallback-static-page", destination: "/fallback-page.html" }, ], }; }, diff --git a/tests/fixtures/app-basic/public/fallback-page.html b/tests/fixtures/app-basic/public/fallback-page.html new file mode 100644 index 000000000..f3bb6b8eb --- /dev/null +++ b/tests/fixtures/app-basic/public/fallback-page.html @@ -0,0 +1,5 @@ + + +Fallback Page +

Hello from fallback static HTML

+ diff --git a/tests/fixtures/pages-basic/next.config.mjs b/tests/fixtures/pages-basic/next.config.mjs index f20c53ae8..168106779 100644 --- a/tests/fixtures/pages-basic/next.config.mjs +++ b/tests/fixtures/pages-basic/next.config.mjs @@ -59,6 +59,11 @@ const nextConfig = { source: "/fallback-rewrite", destination: "/about", }, + // Used by Vitest: pages-router.test.ts (fallback rewrite to static HTML in public/) + { + source: "/fallback-static-page", + destination: "/fallback-page.html", + }, ], }; }, diff --git a/tests/fixtures/pages-basic/public/fallback-page.html b/tests/fixtures/pages-basic/public/fallback-page.html new file mode 100644 index 000000000..f3bb6b8eb --- /dev/null +++ b/tests/fixtures/pages-basic/public/fallback-page.html @@ -0,0 +1,5 @@ + + +Fallback Page +

Hello from fallback static HTML

+ diff --git a/tests/pages-router.test.ts b/tests/pages-router.test.ts index 1b0890799..77e289a6c 100644 --- a/tests/pages-router.test.ts +++ b/tests/pages-router.test.ts @@ -308,6 +308,16 @@ describe("Pages Router integration", () => { expect(res.headers.get("content-type")).toMatch(/text\/html/i); }); + it("serves static HTML file from public/ when fallback rewrite points to .html path", async () => { + // Fallback rewrites run after route matching fails. /fallback-static-page has no + // matching pages route, so the fallback rewrite maps it to /fallback-page.html in public/. + const res = await fetch(`${baseUrl}/fallback-static-page`); + expect(res.status).toBe(200); + const html = await res.text(); + expect(html).toContain("Hello from fallback static HTML"); + expect(res.headers.get("content-type")).toMatch(/text\/html/i); + }); + it("applies fallback rewrites from next.config.js", async () => { const res = await fetch(`${baseUrl}/fallback-rewrite`); expect(res.status).toBe(200); From 945b9cc3e97a544425ff25acdbc0d8f8fdbc4817 Mon Sep 17 00:00:00 2001 From: MD YUNUS Date: Mon, 9 Mar 2026 03:48:09 +0530 Subject: [PATCH 04/14] fix: use path.resolve for publicDir consistency with index.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address bonk review: path.join → path.resolve for publicDir computation in app-dev-server.ts, matching the pattern used in index.ts. path.resolve guarantees a fully normalized absolute path, making the traversal guard comparison more robust. --- packages/vinext/src/server/app-dev-server.ts | 2 +- tests/app-router.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vinext/src/server/app-dev-server.ts b/packages/vinext/src/server/app-dev-server.ts index e48dfed95..1c1d2bb4a 100644 --- a/packages/vinext/src/server/app-dev-server.ts +++ b/packages/vinext/src/server/app-dev-server.ts @@ -67,7 +67,7 @@ export function generateRscEntry( // appDir is something like /project/app or /project/src/app; root is the Vite root. // We need `root` for correctness — path.dirname(appDir) is wrong for src/app layouts // (e.g. /project/src/public instead of /project/public). - const publicDir = root ? path.join(root, "public") : null; + const publicDir = root ? path.resolve(root, "public") : null; // Build import map for all page and layout files const imports: string[] = []; const importMap: Map = new Map(); diff --git a/tests/app-router.test.ts b/tests/app-router.test.ts index d75b50507..0a4bf13da 100644 --- a/tests/app-router.test.ts +++ b/tests/app-router.test.ts @@ -2190,8 +2190,8 @@ describe("App Router next.config.js features (generateRscEntry)", () => { // When root is provided, the generated code should contain that public path // so it can serve .html files from public/ when a rewrite produces a .html path. const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {}, null, "/tmp/test"); - // path.join uses OS separators; the generated code embeds via JSON.stringify - const expectedPublicDir = path.join("/tmp/test", "public"); + // path.resolve produces a fully normalized absolute path; the generated code embeds via JSON.stringify + const expectedPublicDir = path.resolve("/tmp/test", "public"); expect(code).toContain(JSON.stringify(expectedPublicDir)); // __publicDir should be hoisted to module scope expect(code).toContain("const __publicDir = " + JSON.stringify(expectedPublicDir)); From a6852a165da68309b8711de81920bab9529cc042 Mon Sep 17 00:00:00 2001 From: MD YUNUS <115855149+yunus25jmi1@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:47:34 +0530 Subject: [PATCH 05/14] Update packages/vinext/src/index.ts Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> --- packages/vinext/src/index.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 41e0811e2..d42a3618a 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -3092,7 +3092,16 @@ hydrate(); if (path.extname(afterFilesPathname)) { // "." + afterFilesPathname works because rewrite destinations always start with "/" const publicFilePath = path.resolve(resolvedPublicDir, "." + afterFilesPathname); - if (publicFilePath.startsWith(resolvedPublicDir + path.sep) && fs.existsSync(publicFilePath) && fs.statSync(publicFilePath).isFile()) { + try { + const stat = fs.statSync(publicFilePath); + if (stat.isFile()) { + const content = fs.readFileSync(publicFilePath); + const ext = (path.extname(afterFilesPathname).slice(1)).toLowerCase(); + res.writeHead(200, { "Content-Type": mimeType(ext) }); + res.end(content); + return; + } + } catch (e) { if (e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', e); } const content = fs.readFileSync(publicFilePath); const ext = (path.extname(afterFilesPathname).slice(1)).toLowerCase(); res.writeHead(200, { "Content-Type": mimeType(ext) }); From e6296958665839162cda9b4497af1e0e87dbdfdc Mon Sep 17 00:00:00 2001 From: MD YUNUS <115855149+yunus25jmi1@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:48:05 +0530 Subject: [PATCH 06/14] Update packages/vinext/src/index.ts Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> --- packages/vinext/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index d42a3618a..62a87004a 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -3078,7 +3078,7 @@ hydrate(); nextConfig.rewrites.afterFiles, reqCtx, ); - if (afterRewrite) { + resolvedUrl = afterRewrite; // External rewrite from afterFiles — proxy to external URL if (isExternalUrl(afterRewrite)) { await proxyExternalRewriteNode(req, res, afterRewrite); From cab61157330704583a5599b7b1a0618ce95d14dc Mon Sep 17 00:00:00 2001 From: MD YUNUS Date: Mon, 9 Mar 2026 22:06:43 +0530 Subject: [PATCH 07/14] fix syntax errors in merge resolution - Add missing try-catch blocks for proper error handling - Fix type annotation for error catching - Ensure proper brace matching for middleware function --- packages/vinext/src/index.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 4a2924be2..5bd407c3d 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -2123,13 +2123,13 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { nextConfig.rewrites.afterFiles, reqCtx, ); + if (afterRewrite) { resolvedUrl = afterRewrite; // External rewrite from afterFiles — proxy to external URL if (isExternalUrl(afterRewrite)) { await proxyExternalRewriteNode(req, res, afterRewrite); return; } - resolvedUrl = afterRewrite; // If the rewritten path has a file extension, it may point to a // static file in public/. Serve it directly before route matching // (which would miss it and SSR would return 404). @@ -2146,13 +2146,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { res.end(content); return; } - } catch (e) { if (e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', e); } - const content = fs.readFileSync(publicFilePath); - const ext = (path.extname(afterFilesPathname).slice(1)).toLowerCase(); - res.writeHead(200, { "Content-Type": mimeType(ext) }); - res.end(content); - return; - } + } catch (e: any) { if (e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', e); } } } } @@ -2214,7 +2208,11 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // otherwise render via the pages SSR handler (will 404 for unknown routes). if (hasAppDir) return next(); - await handler(req, res, resolvedUrl, mwStatus); + try { + await handler(req, res, resolvedUrl, mwStatus); + } catch (e) { + next(e); + } } catch (e) { next(e); } From e30bd8b016b630fca7aae5167b8727009ce490dd Mon Sep 17 00:00:00 2001 From: MD YUNUS Date: Mon, 9 Mar 2026 22:15:33 +0530 Subject: [PATCH 08/14] fix: update snapshots and formatting for static file serving feature - Update entry template snapshots to include new imports and static file logic - Fix code formatting across all affected files - All tests now pass including format checks --- CLAUDE.md | 2 +- packages/vinext/src/index.ts | 20 +- packages/vinext/src/server/prod-server.ts | 10 +- .../entry-templates.test.ts.snap | 254 +++++++++++++++++- tests/app-router.test.ts | 13 +- .../app-basic/public/auth/no-access.html | 10 +- .../app-basic/public/fallback-page.html | 11 +- .../app-basic/public/static-html-page.html | 11 +- .../pages-basic/public/auth/no-access.html | 10 +- .../pages-basic/public/fallback-page.html | 11 +- .../pages-basic/public/static-html-page.html | 11 +- 11 files changed, 335 insertions(+), 28 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 47dc3e3d8..c31706425 120000 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1 @@ -AGENTS.md \ No newline at end of file +AGENTS.md diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 5bd407c3d..404e272a4 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -2136,17 +2136,23 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { const afterFilesPathname = afterRewrite.split("?")[0]; if (path.extname(afterFilesPathname)) { // "." + afterFilesPathname works because rewrite destinations always start with "/" - const publicFilePath = path.resolve(resolvedPublicDir, "." + afterFilesPathname); + const publicFilePath = path.resolve( + resolvedPublicDir, + "." + afterFilesPathname, + ); try { const stat = fs.statSync(publicFilePath); if (stat.isFile()) { const content = fs.readFileSync(publicFilePath); - const ext = (path.extname(afterFilesPathname).slice(1)).toLowerCase(); + const ext = path.extname(afterFilesPathname).slice(1).toLowerCase(); res.writeHead(200, { "Content-Type": mimeType(ext) }); res.end(content); return; } - } catch (e: any) { if (e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', e); } + } catch (e: any) { + if (e?.code !== "ENOENT") + console.warn("[vinext] static file check failed:", e); + } } } } @@ -2191,9 +2197,13 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (path.extname(fallbackPathname)) { // "." + fallbackPathname: see afterFiles comment above — leading "/" is assumed const publicFilePath = path.resolve(resolvedPublicDir, "." + fallbackPathname); - if (publicFilePath.startsWith(resolvedPublicDir + path.sep) && fs.existsSync(publicFilePath) && fs.statSync(publicFilePath).isFile()) { + if ( + publicFilePath.startsWith(resolvedPublicDir + path.sep) && + fs.existsSync(publicFilePath) && + fs.statSync(publicFilePath).isFile() + ) { const content = fs.readFileSync(publicFilePath); - const ext = (path.extname(fallbackPathname).slice(1)).toLowerCase(); + const ext = path.extname(fallbackPathname).slice(1).toLowerCase(); res.writeHead(200, { "Content-Type": mimeType(ext) }); res.end(content); return; diff --git a/packages/vinext/src/server/prod-server.ts b/packages/vinext/src/server/prod-server.ts index fde328e08..4bb825277 100644 --- a/packages/vinext/src/server/prod-server.ts +++ b/packages/vinext/src/server/prod-server.ts @@ -1009,7 +1009,10 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { // If the rewritten path has a file extension, it may point to a static // file in public/ (copied to clientDir during build). Try to serve it // directly before falling through to SSR (which would return 404). - if (path.extname(resolvedPathname) && tryServeStatic(req, res, clientDir, resolvedPathname, compress)) { + if ( + path.extname(resolvedPathname) && + tryServeStatic(req, res, clientDir, resolvedPathname, compress) + ) { return; } } @@ -1035,7 +1038,10 @@ async function startPagesRouterServer(options: PagesRouterServerOptions) { } // Check if fallback targets a static file in public/ const fallbackPathname = fallbackRewrite.split("?")[0]; - if (path.extname(fallbackPathname) && tryServeStatic(req, res, clientDir, fallbackPathname, compress)) { + if ( + path.extname(fallbackPathname) && + tryServeStatic(req, res, clientDir, fallbackPathname, compress) + ) { return; } response = await renderPage(webRequest, fallbackRewrite, ssrManifest); diff --git a/tests/__snapshots__/entry-templates.test.ts.snap b/tests/__snapshots__/entry-templates.test.ts.snap index af1cb3241..62339687f 100644 --- a/tests/__snapshots__/entry-templates.test.ts.snap +++ b/tests/__snapshots__/entry-templates.test.ts.snap @@ -335,6 +335,8 @@ main(); exports[`App Router entry templates > generateRscEntry snapshot (minimal routes) 1`] = ` " +import __nodeFs from "node:fs"; +import __nodePath from "node:path"; import { renderToReadableStream, decodeReply, @@ -1300,6 +1302,8 @@ const __configRedirects = []; const __configRewrites = {"beforeFiles":[],"afterFiles":[],"fallback":[]}; const __configHeaders = []; const __allowedOrigins = []; +const __mimeTypes = {"html":"text/html; charset=utf-8","htm":"text/html; charset=utf-8","css":"text/css","js":"application/javascript","mjs":"application/javascript","cjs":"application/javascript","json":"application/json","txt":"text/plain","xml":"application/xml","svg":"image/svg+xml","png":"image/png","jpg":"image/jpeg","jpeg":"image/jpeg","gif":"image/gif","webp":"image/webp","avif":"image/avif","ico":"image/x-icon","woff":"font/woff","woff2":"font/woff2","ttf":"font/ttf","otf":"font/otf","eot":"application/vnd.ms-fontobject","pdf":"application/pdf","map":"application/json","mp4":"video/mp4","webm":"video/webm","mp3":"audio/mpeg","ogg":"audio/ogg","wasm":"application/wasm"}; +const __publicDir = null; // ── Dev server origin verification ────────────────────────────────────── @@ -1908,6 +1912,26 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { return proxyExternalRequest(request, __afterRewritten); } cleanPathname = __afterRewritten; + // If the rewritten path has a file extension, it may point to a static + // file in public/. Serve it directly before route matching. + const __afterExtname = __nodePath.extname(cleanPathname); + if (__afterExtname && __publicDir !== null) { + // "." + cleanPathname works because rewrite destinations always start with "/"; + // the traversal guard below catches any malformed path regardless. + const __afterPublicFile = __nodePath.resolve(__publicDir, "." + cleanPathname); + if (__afterPublicFile.startsWith(__publicDir + __nodePath.sep)) { + try { + const __afterStat = __nodeFs.statSync(__afterPublicFile); + if (__afterStat.isFile()) { + const __afterContent = __nodeFs.readFileSync(__afterPublicFile); + const __afterExt = __afterExtname.slice(1).toLowerCase(); + setHeadersContext(null); + setNavigationContext(null); + return new Response(__afterContent, { status: 200, headers: { "Content-Type": __mimeTypes[__afterExt] ?? "application/octet-stream" } }); + } + } catch (__e) { if (__e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', __e); } + } + } } } @@ -1923,6 +1947,24 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { return proxyExternalRequest(request, __fallbackRewritten); } cleanPathname = __fallbackRewritten; + // Check if fallback targets a static file in public/ + const __fbExtname = __nodePath.extname(cleanPathname); + if (__fbExtname && __publicDir !== null) { + // "." + cleanPathname: see afterFiles comment above — leading "/" is assumed. + const __fbPublicFile = __nodePath.resolve(__publicDir, "." + cleanPathname); + if (__fbPublicFile.startsWith(__publicDir + __nodePath.sep)) { + try { + const __fbStat = __nodeFs.statSync(__fbPublicFile); + if (__fbStat.isFile()) { + const __fbContent = __nodeFs.readFileSync(__fbPublicFile); + const __fbExt = __fbExtname.slice(1).toLowerCase(); + setHeadersContext(null); + setNavigationContext(null); + return new Response(__fbContent, { status: 200, headers: { "Content-Type": __mimeTypes[__fbExt] ?? "application/octet-stream" } }); + } + } catch (__e) { if (__e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', __e); } + } + } match = matchRoute(cleanPathname, routes); } } @@ -2622,6 +2664,8 @@ if (import.meta.hot) { exports[`App Router entry templates > generateRscEntry snapshot (with config) 1`] = ` " +import __nodeFs from "node:fs"; +import __nodePath from "node:path"; import { renderToReadableStream, decodeReply, @@ -3587,6 +3631,8 @@ const __configRedirects = [{"source":"/old","destination":"/new","permanent":tru const __configRewrites = {"beforeFiles":[{"source":"/api/:path*","destination":"/backend/:path*"}],"afterFiles":[],"fallback":[]}; const __configHeaders = [{"source":"/api/:path*","headers":[{"key":"X-Custom","value":"test"}]}]; const __allowedOrigins = ["https://example.com"]; +const __mimeTypes = {"html":"text/html; charset=utf-8","htm":"text/html; charset=utf-8","css":"text/css","js":"application/javascript","mjs":"application/javascript","cjs":"application/javascript","json":"application/json","txt":"text/plain","xml":"application/xml","svg":"image/svg+xml","png":"image/png","jpg":"image/jpeg","jpeg":"image/jpeg","gif":"image/gif","webp":"image/webp","avif":"image/avif","ico":"image/x-icon","woff":"font/woff","woff2":"font/woff2","ttf":"font/ttf","otf":"font/otf","eot":"application/vnd.ms-fontobject","pdf":"application/pdf","map":"application/json","mp4":"video/mp4","webm":"video/webm","mp3":"audio/mpeg","ogg":"audio/ogg","wasm":"application/wasm"}; +const __publicDir = null; // ── Dev server origin verification ────────────────────────────────────── @@ -4198,6 +4244,26 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { return proxyExternalRequest(request, __afterRewritten); } cleanPathname = __afterRewritten; + // If the rewritten path has a file extension, it may point to a static + // file in public/. Serve it directly before route matching. + const __afterExtname = __nodePath.extname(cleanPathname); + if (__afterExtname && __publicDir !== null) { + // "." + cleanPathname works because rewrite destinations always start with "/"; + // the traversal guard below catches any malformed path regardless. + const __afterPublicFile = __nodePath.resolve(__publicDir, "." + cleanPathname); + if (__afterPublicFile.startsWith(__publicDir + __nodePath.sep)) { + try { + const __afterStat = __nodeFs.statSync(__afterPublicFile); + if (__afterStat.isFile()) { + const __afterContent = __nodeFs.readFileSync(__afterPublicFile); + const __afterExt = __afterExtname.slice(1).toLowerCase(); + setHeadersContext(null); + setNavigationContext(null); + return new Response(__afterContent, { status: 200, headers: { "Content-Type": __mimeTypes[__afterExt] ?? "application/octet-stream" } }); + } + } catch (__e) { if (__e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', __e); } + } + } } } @@ -4213,6 +4279,24 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { return proxyExternalRequest(request, __fallbackRewritten); } cleanPathname = __fallbackRewritten; + // Check if fallback targets a static file in public/ + const __fbExtname = __nodePath.extname(cleanPathname); + if (__fbExtname && __publicDir !== null) { + // "." + cleanPathname: see afterFiles comment above — leading "/" is assumed. + const __fbPublicFile = __nodePath.resolve(__publicDir, "." + cleanPathname); + if (__fbPublicFile.startsWith(__publicDir + __nodePath.sep)) { + try { + const __fbStat = __nodeFs.statSync(__fbPublicFile); + if (__fbStat.isFile()) { + const __fbContent = __nodeFs.readFileSync(__fbPublicFile); + const __fbExt = __fbExtname.slice(1).toLowerCase(); + setHeadersContext(null); + setNavigationContext(null); + return new Response(__fbContent, { status: 200, headers: { "Content-Type": __mimeTypes[__fbExt] ?? "application/octet-stream" } }); + } + } catch (__e) { if (__e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', __e); } + } + } match = matchRoute(cleanPathname, routes); } } @@ -4912,6 +4996,8 @@ if (import.meta.hot) { exports[`App Router entry templates > generateRscEntry snapshot (with global error) 1`] = ` " +import __nodeFs from "node:fs"; +import __nodePath from "node:path"; import { renderToReadableStream, decodeReply, @@ -5907,6 +5993,8 @@ const __configRedirects = []; const __configRewrites = {"beforeFiles":[],"afterFiles":[],"fallback":[]}; const __configHeaders = []; const __allowedOrigins = []; +const __mimeTypes = {"html":"text/html; charset=utf-8","htm":"text/html; charset=utf-8","css":"text/css","js":"application/javascript","mjs":"application/javascript","cjs":"application/javascript","json":"application/json","txt":"text/plain","xml":"application/xml","svg":"image/svg+xml","png":"image/png","jpg":"image/jpeg","jpeg":"image/jpeg","gif":"image/gif","webp":"image/webp","avif":"image/avif","ico":"image/x-icon","woff":"font/woff","woff2":"font/woff2","ttf":"font/ttf","otf":"font/otf","eot":"application/vnd.ms-fontobject","pdf":"application/pdf","map":"application/json","mp4":"video/mp4","webm":"video/webm","mp3":"audio/mpeg","ogg":"audio/ogg","wasm":"application/wasm"}; +const __publicDir = null; // ── Dev server origin verification ────────────────────────────────────── @@ -6515,6 +6603,26 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { return proxyExternalRequest(request, __afterRewritten); } cleanPathname = __afterRewritten; + // If the rewritten path has a file extension, it may point to a static + // file in public/. Serve it directly before route matching. + const __afterExtname = __nodePath.extname(cleanPathname); + if (__afterExtname && __publicDir !== null) { + // "." + cleanPathname works because rewrite destinations always start with "/"; + // the traversal guard below catches any malformed path regardless. + const __afterPublicFile = __nodePath.resolve(__publicDir, "." + cleanPathname); + if (__afterPublicFile.startsWith(__publicDir + __nodePath.sep)) { + try { + const __afterStat = __nodeFs.statSync(__afterPublicFile); + if (__afterStat.isFile()) { + const __afterContent = __nodeFs.readFileSync(__afterPublicFile); + const __afterExt = __afterExtname.slice(1).toLowerCase(); + setHeadersContext(null); + setNavigationContext(null); + return new Response(__afterContent, { status: 200, headers: { "Content-Type": __mimeTypes[__afterExt] ?? "application/octet-stream" } }); + } + } catch (__e) { if (__e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', __e); } + } + } } } @@ -6530,6 +6638,24 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { return proxyExternalRequest(request, __fallbackRewritten); } cleanPathname = __fallbackRewritten; + // Check if fallback targets a static file in public/ + const __fbExtname = __nodePath.extname(cleanPathname); + if (__fbExtname && __publicDir !== null) { + // "." + cleanPathname: see afterFiles comment above — leading "/" is assumed. + const __fbPublicFile = __nodePath.resolve(__publicDir, "." + cleanPathname); + if (__fbPublicFile.startsWith(__publicDir + __nodePath.sep)) { + try { + const __fbStat = __nodeFs.statSync(__fbPublicFile); + if (__fbStat.isFile()) { + const __fbContent = __nodeFs.readFileSync(__fbPublicFile); + const __fbExt = __fbExtname.slice(1).toLowerCase(); + setHeadersContext(null); + setNavigationContext(null); + return new Response(__fbContent, { status: 200, headers: { "Content-Type": __mimeTypes[__fbExt] ?? "application/octet-stream" } }); + } + } catch (__e) { if (__e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', __e); } + } + } match = matchRoute(cleanPathname, routes); } } @@ -7237,6 +7363,8 @@ if (import.meta.hot) { exports[`App Router entry templates > generateRscEntry snapshot (with instrumentation) 1`] = ` " +import __nodeFs from "node:fs"; +import __nodePath from "node:path"; import { renderToReadableStream, decodeReply, @@ -8231,6 +8359,8 @@ const __configRedirects = []; const __configRewrites = {"beforeFiles":[],"afterFiles":[],"fallback":[]}; const __configHeaders = []; const __allowedOrigins = []; +const __mimeTypes = {"html":"text/html; charset=utf-8","htm":"text/html; charset=utf-8","css":"text/css","js":"application/javascript","mjs":"application/javascript","cjs":"application/javascript","json":"application/json","txt":"text/plain","xml":"application/xml","svg":"image/svg+xml","png":"image/png","jpg":"image/jpeg","jpeg":"image/jpeg","gif":"image/gif","webp":"image/webp","avif":"image/avif","ico":"image/x-icon","woff":"font/woff","woff2":"font/woff2","ttf":"font/ttf","otf":"font/otf","eot":"application/vnd.ms-fontobject","pdf":"application/pdf","map":"application/json","mp4":"video/mp4","webm":"video/webm","mp3":"audio/mpeg","ogg":"audio/ogg","wasm":"application/wasm"}; +const __publicDir = null; // ── Dev server origin verification ────────────────────────────────────── @@ -8842,6 +8972,26 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { return proxyExternalRequest(request, __afterRewritten); } cleanPathname = __afterRewritten; + // If the rewritten path has a file extension, it may point to a static + // file in public/. Serve it directly before route matching. + const __afterExtname = __nodePath.extname(cleanPathname); + if (__afterExtname && __publicDir !== null) { + // "." + cleanPathname works because rewrite destinations always start with "/"; + // the traversal guard below catches any malformed path regardless. + const __afterPublicFile = __nodePath.resolve(__publicDir, "." + cleanPathname); + if (__afterPublicFile.startsWith(__publicDir + __nodePath.sep)) { + try { + const __afterStat = __nodeFs.statSync(__afterPublicFile); + if (__afterStat.isFile()) { + const __afterContent = __nodeFs.readFileSync(__afterPublicFile); + const __afterExt = __afterExtname.slice(1).toLowerCase(); + setHeadersContext(null); + setNavigationContext(null); + return new Response(__afterContent, { status: 200, headers: { "Content-Type": __mimeTypes[__afterExt] ?? "application/octet-stream" } }); + } + } catch (__e) { if (__e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', __e); } + } + } } } @@ -8857,6 +9007,24 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { return proxyExternalRequest(request, __fallbackRewritten); } cleanPathname = __fallbackRewritten; + // Check if fallback targets a static file in public/ + const __fbExtname = __nodePath.extname(cleanPathname); + if (__fbExtname && __publicDir !== null) { + // "." + cleanPathname: see afterFiles comment above — leading "/" is assumed. + const __fbPublicFile = __nodePath.resolve(__publicDir, "." + cleanPathname); + if (__fbPublicFile.startsWith(__publicDir + __nodePath.sep)) { + try { + const __fbStat = __nodeFs.statSync(__fbPublicFile); + if (__fbStat.isFile()) { + const __fbContent = __nodeFs.readFileSync(__fbPublicFile); + const __fbExt = __fbExtname.slice(1).toLowerCase(); + setHeadersContext(null); + setNavigationContext(null); + return new Response(__fbContent, { status: 200, headers: { "Content-Type": __mimeTypes[__fbExt] ?? "application/octet-stream" } }); + } + } catch (__e) { if (__e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', __e); } + } + } match = matchRoute(cleanPathname, routes); } } @@ -9556,6 +9724,8 @@ if (import.meta.hot) { exports[`App Router entry templates > generateRscEntry snapshot (with metadata routes) 1`] = ` " +import __nodeFs from "node:fs"; +import __nodePath from "node:path"; import { renderToReadableStream, decodeReply, @@ -10528,6 +10698,8 @@ const __configRedirects = []; const __configRewrites = {"beforeFiles":[],"afterFiles":[],"fallback":[]}; const __configHeaders = []; const __allowedOrigins = []; +const __mimeTypes = {"html":"text/html; charset=utf-8","htm":"text/html; charset=utf-8","css":"text/css","js":"application/javascript","mjs":"application/javascript","cjs":"application/javascript","json":"application/json","txt":"text/plain","xml":"application/xml","svg":"image/svg+xml","png":"image/png","jpg":"image/jpeg","jpeg":"image/jpeg","gif":"image/gif","webp":"image/webp","avif":"image/avif","ico":"image/x-icon","woff":"font/woff","woff2":"font/woff2","ttf":"font/ttf","otf":"font/otf","eot":"application/vnd.ms-fontobject","pdf":"application/pdf","map":"application/json","mp4":"video/mp4","webm":"video/webm","mp3":"audio/mpeg","ogg":"audio/ogg","wasm":"application/wasm"}; +const __publicDir = null; // ── Dev server origin verification ────────────────────────────────────── @@ -11136,6 +11308,26 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { return proxyExternalRequest(request, __afterRewritten); } cleanPathname = __afterRewritten; + // If the rewritten path has a file extension, it may point to a static + // file in public/. Serve it directly before route matching. + const __afterExtname = __nodePath.extname(cleanPathname); + if (__afterExtname && __publicDir !== null) { + // "." + cleanPathname works because rewrite destinations always start with "/"; + // the traversal guard below catches any malformed path regardless. + const __afterPublicFile = __nodePath.resolve(__publicDir, "." + cleanPathname); + if (__afterPublicFile.startsWith(__publicDir + __nodePath.sep)) { + try { + const __afterStat = __nodeFs.statSync(__afterPublicFile); + if (__afterStat.isFile()) { + const __afterContent = __nodeFs.readFileSync(__afterPublicFile); + const __afterExt = __afterExtname.slice(1).toLowerCase(); + setHeadersContext(null); + setNavigationContext(null); + return new Response(__afterContent, { status: 200, headers: { "Content-Type": __mimeTypes[__afterExt] ?? "application/octet-stream" } }); + } + } catch (__e) { if (__e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', __e); } + } + } } } @@ -11151,6 +11343,24 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { return proxyExternalRequest(request, __fallbackRewritten); } cleanPathname = __fallbackRewritten; + // Check if fallback targets a static file in public/ + const __fbExtname = __nodePath.extname(cleanPathname); + if (__fbExtname && __publicDir !== null) { + // "." + cleanPathname: see afterFiles comment above — leading "/" is assumed. + const __fbPublicFile = __nodePath.resolve(__publicDir, "." + cleanPathname); + if (__fbPublicFile.startsWith(__publicDir + __nodePath.sep)) { + try { + const __fbStat = __nodeFs.statSync(__fbPublicFile); + if (__fbStat.isFile()) { + const __fbContent = __nodeFs.readFileSync(__fbPublicFile); + const __fbExt = __fbExtname.slice(1).toLowerCase(); + setHeadersContext(null); + setNavigationContext(null); + return new Response(__fbContent, { status: 200, headers: { "Content-Type": __mimeTypes[__fbExt] ?? "application/octet-stream" } }); + } + } catch (__e) { if (__e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', __e); } + } + } match = matchRoute(cleanPathname, routes); } } @@ -11850,6 +12060,8 @@ if (import.meta.hot) { exports[`App Router entry templates > generateRscEntry snapshot (with middleware) 1`] = ` " +import __nodeFs from "node:fs"; +import __nodePath from "node:path"; import { renderToReadableStream, decodeReply, @@ -12853,6 +13065,8 @@ const __configRedirects = []; const __configRewrites = {"beforeFiles":[],"afterFiles":[],"fallback":[]}; const __configHeaders = []; const __allowedOrigins = []; +const __mimeTypes = {"html":"text/html; charset=utf-8","htm":"text/html; charset=utf-8","css":"text/css","js":"application/javascript","mjs":"application/javascript","cjs":"application/javascript","json":"application/json","txt":"text/plain","xml":"application/xml","svg":"image/svg+xml","png":"image/png","jpg":"image/jpeg","jpeg":"image/jpeg","gif":"image/gif","webp":"image/webp","avif":"image/avif","ico":"image/x-icon","woff":"font/woff","woff2":"font/woff2","ttf":"font/ttf","otf":"font/otf","eot":"application/vnd.ms-fontobject","pdf":"application/pdf","map":"application/json","mp4":"video/mp4","webm":"video/webm","mp3":"audio/mpeg","ogg":"audio/ogg","wasm":"application/wasm"}; +const __publicDir = null; // ── Dev server origin verification ────────────────────────────────────── @@ -13543,6 +13757,26 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { return proxyExternalRequest(request, __afterRewritten); } cleanPathname = __afterRewritten; + // If the rewritten path has a file extension, it may point to a static + // file in public/. Serve it directly before route matching. + const __afterExtname = __nodePath.extname(cleanPathname); + if (__afterExtname && __publicDir !== null) { + // "." + cleanPathname works because rewrite destinations always start with "/"; + // the traversal guard below catches any malformed path regardless. + const __afterPublicFile = __nodePath.resolve(__publicDir, "." + cleanPathname); + if (__afterPublicFile.startsWith(__publicDir + __nodePath.sep)) { + try { + const __afterStat = __nodeFs.statSync(__afterPublicFile); + if (__afterStat.isFile()) { + const __afterContent = __nodeFs.readFileSync(__afterPublicFile); + const __afterExt = __afterExtname.slice(1).toLowerCase(); + setHeadersContext(null); + setNavigationContext(null); + return new Response(__afterContent, { status: 200, headers: { "Content-Type": __mimeTypes[__afterExt] ?? "application/octet-stream" } }); + } + } catch (__e) { if (__e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', __e); } + } + } } } @@ -13558,6 +13792,24 @@ async function _handleRequest(request, __reqCtx, _mwCtx) { return proxyExternalRequest(request, __fallbackRewritten); } cleanPathname = __fallbackRewritten; + // Check if fallback targets a static file in public/ + const __fbExtname = __nodePath.extname(cleanPathname); + if (__fbExtname && __publicDir !== null) { + // "." + cleanPathname: see afterFiles comment above — leading "/" is assumed. + const __fbPublicFile = __nodePath.resolve(__publicDir, "." + cleanPathname); + if (__fbPublicFile.startsWith(__publicDir + __nodePath.sep)) { + try { + const __fbStat = __nodeFs.statSync(__fbPublicFile); + if (__fbStat.isFile()) { + const __fbContent = __nodeFs.readFileSync(__fbPublicFile); + const __fbExt = __fbExtname.slice(1).toLowerCase(); + setHeadersContext(null); + setNavigationContext(null); + return new Response(__fbContent, { status: 200, headers: { "Content-Type": __mimeTypes[__fbExt] ?? "application/octet-stream" } }); + } + } catch (__e) { if (__e?.code !== 'ENOENT') console.warn('[vinext] static file check failed:', __e); } + } + } match = matchRoute(cleanPathname, routes); } } @@ -14827,7 +15079,7 @@ if (typeof _instrumentation.onRequestError === "function") { const i18nConfig = null; // Full resolved config for production server (embedded at build time) -export const vinextConfig = {"basePath":"","trailingSlash":false,"redirects":[{"source":"/old-about","destination":"/about","permanent":true}],"rewrites":{"beforeFiles":[{"source":"/before-rewrite","destination":"/about"},{"source":"/mw-gated-before","has":[{"type":"cookie","key":"mw-before-user"}],"destination":"/about"}],"afterFiles":[{"source":"/after-rewrite","destination":"/about"},{"source":"/mw-gated-rewrite","has":[{"type":"cookie","key":"mw-user"}],"destination":"/about"}],"fallback":[{"source":"/fallback-rewrite","destination":"/about"}]},"headers":[{"source":"/api/(.*)","headers":[{"key":"X-Custom-Header","value":"vinext"}]},{"source":"/about","has":[{"type":"cookie","key":"logged-in"}],"headers":[{"key":"X-Auth-Only-Header","value":"1"}]},{"source":"/about","missing":[{"type":"cookie","key":"logged-in"}],"headers":[{"key":"X-Guest-Only-Header","value":"1"}]},{"source":"/ssr","headers":[{"key":"Vary","value":"Accept-Language"}]}],"i18n":null,"images":{}}; +export const vinextConfig = {"basePath":"","trailingSlash":false,"redirects":[{"source":"/old-about","destination":"/about","permanent":true}],"rewrites":{"beforeFiles":[{"source":"/before-rewrite","destination":"/about"},{"source":"/mw-gated-before","has":[{"type":"cookie","key":"mw-before-user"}],"destination":"/about"}],"afterFiles":[{"source":"/after-rewrite","destination":"/about"},{"source":"/mw-gated-rewrite","has":[{"type":"cookie","key":"mw-user"}],"destination":"/about"},{"source":"/static-html-page","destination":"/static-html-page.html"},{"source":"/auth/no-access","destination":"/auth/no-access.html"}],"fallback":[{"source":"/fallback-rewrite","destination":"/about"},{"source":"/fallback-static-page","destination":"/fallback-page.html"}]},"headers":[{"source":"/api/(.*)","headers":[{"key":"X-Custom-Header","value":"vinext"}]},{"source":"/about","has":[{"type":"cookie","key":"logged-in"}],"headers":[{"key":"X-Auth-Only-Header","value":"1"}]},{"source":"/about","missing":[{"type":"cookie","key":"logged-in"}],"headers":[{"key":"X-Guest-Only-Header","value":"1"}]},{"source":"/ssr","headers":[{"key":"Vary","value":"Accept-Language"}]}],"i18n":null,"images":{}}; // ISR cache helpers (inlined for the server entry) async function isrGet(key) { diff --git a/tests/app-router.test.ts b/tests/app-router.test.ts index 166bfdb43..63b628487 100644 --- a/tests/app-router.test.ts +++ b/tests/app-router.test.ts @@ -2153,7 +2153,18 @@ describe("App Router next.config.js features (generateRscEntry)", () => { it("embeds root/public path for serving static files after rewrite", () => { // When root is provided, the generated code should contain that public path // so it can serve .html files from public/ when a rewrite produces a .html path. - const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {}, null, "/tmp/test"); + const code = generateRscEntry( + "/tmp/test/app", + minimalRoutes, + null, + [], + null, + "", + false, + {}, + null, + "/tmp/test", + ); // path.resolve produces a fully normalized absolute path; the generated code embeds via JSON.stringify const expectedPublicDir = path.resolve("/tmp/test", "public"); expect(code).toContain(JSON.stringify(expectedPublicDir)); diff --git a/tests/fixtures/app-basic/public/auth/no-access.html b/tests/fixtures/app-basic/public/auth/no-access.html index 1f7745666..19034aac3 100644 --- a/tests/fixtures/app-basic/public/auth/no-access.html +++ b/tests/fixtures/app-basic/public/auth/no-access.html @@ -1,5 +1,9 @@ - + -No Access -Access denied from nested static HTML + + No Access + + + Access denied from nested static HTML + diff --git a/tests/fixtures/app-basic/public/fallback-page.html b/tests/fixtures/app-basic/public/fallback-page.html index f3bb6b8eb..44ad71659 100644 --- a/tests/fixtures/app-basic/public/fallback-page.html +++ b/tests/fixtures/app-basic/public/fallback-page.html @@ -1,5 +1,10 @@ - + -Fallback Page -

Hello from fallback static HTML

+ + + Fallback Page + + +

Hello from fallback static HTML

+ diff --git a/tests/fixtures/app-basic/public/static-html-page.html b/tests/fixtures/app-basic/public/static-html-page.html index 36a9bc316..07c276cf5 100644 --- a/tests/fixtures/app-basic/public/static-html-page.html +++ b/tests/fixtures/app-basic/public/static-html-page.html @@ -1,5 +1,10 @@ - + -Static HTML Page -

Hello from static HTML

+ + + Static HTML Page + + +

Hello from static HTML

+ diff --git a/tests/fixtures/pages-basic/public/auth/no-access.html b/tests/fixtures/pages-basic/public/auth/no-access.html index 1f7745666..19034aac3 100644 --- a/tests/fixtures/pages-basic/public/auth/no-access.html +++ b/tests/fixtures/pages-basic/public/auth/no-access.html @@ -1,5 +1,9 @@ - + -No Access -Access denied from nested static HTML + + No Access + + + Access denied from nested static HTML + diff --git a/tests/fixtures/pages-basic/public/fallback-page.html b/tests/fixtures/pages-basic/public/fallback-page.html index f3bb6b8eb..44ad71659 100644 --- a/tests/fixtures/pages-basic/public/fallback-page.html +++ b/tests/fixtures/pages-basic/public/fallback-page.html @@ -1,5 +1,10 @@ - + -Fallback Page -

Hello from fallback static HTML

+ + + Fallback Page + + +

Hello from fallback static HTML

+ diff --git a/tests/fixtures/pages-basic/public/static-html-page.html b/tests/fixtures/pages-basic/public/static-html-page.html index 36a9bc316..07c276cf5 100644 --- a/tests/fixtures/pages-basic/public/static-html-page.html +++ b/tests/fixtures/pages-basic/public/static-html-page.html @@ -1,5 +1,10 @@ - + -Static HTML Page -

Hello from static HTML

+ + + Static HTML Page + + +

Hello from static HTML

+ From 60ee65d11303d774cc25ab2cb228749567248d17 Mon Sep 17 00:00:00 2001 From: MD YUNUS <115855149+yunus25jmi1@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:58:19 +0530 Subject: [PATCH 09/14] Update packages/vinext/src/index.ts Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> --- packages/vinext/src/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 404e272a4..569d04032 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -2134,12 +2134,16 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // static file in public/. Serve it directly before route matching // (which would miss it and SSR would return 404). const afterFilesPathname = afterRewrite.split("?")[0]; + const afterFilesPathname = afterRewrite.split("?")[0]; if (path.extname(afterFilesPathname)) { // "." + afterFilesPathname works because rewrite destinations always start with "/" const publicFilePath = path.resolve( resolvedPublicDir, "." + afterFilesPathname, ); + if ( + publicFilePath.startsWith(resolvedPublicDir + path.sep) + ) { try { const stat = fs.statSync(publicFilePath); if (stat.isFile()) { @@ -2153,6 +2157,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (e?.code !== "ENOENT") console.warn("[vinext] static file check failed:", e); } + } } } } From d2f2c8d1ba1fb5a89a7e5bda3fe398d169f18a6a Mon Sep 17 00:00:00 2001 From: MD YUNUS <115855149+yunus25jmi1@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:58:50 +0530 Subject: [PATCH 10/14] Update packages/vinext/src/index.ts Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> --- packages/vinext/src/index.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 569d04032..e1a07ce1b 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -2198,20 +2198,27 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { return; } // Check if fallback targets a static file in public/ + // Check if fallback targets a static file in public/ const fallbackPathname = fallbackRewrite.split("?")[0]; if (path.extname(fallbackPathname)) { // "." + fallbackPathname: see afterFiles comment above — leading "/" is assumed const publicFilePath = path.resolve(resolvedPublicDir, "." + fallbackPathname); if ( - publicFilePath.startsWith(resolvedPublicDir + path.sep) && - fs.existsSync(publicFilePath) && - fs.statSync(publicFilePath).isFile() + publicFilePath.startsWith(resolvedPublicDir + path.sep) ) { - const content = fs.readFileSync(publicFilePath); - const ext = path.extname(fallbackPathname).slice(1).toLowerCase(); - res.writeHead(200, { "Content-Type": mimeType(ext) }); - res.end(content); - return; + try { + const stat = fs.statSync(publicFilePath); + if (stat.isFile()) { + const content = fs.readFileSync(publicFilePath); + const ext = path.extname(fallbackPathname).slice(1).toLowerCase(); + res.writeHead(200, { "Content-Type": mimeType(ext) }); + res.end(content); + return; + } + } catch (e: any) { + if (e?.code !== "ENOENT") + console.warn("[vinext] static file check failed:", e); + } } } await handler(req, res, fallbackRewrite, mwStatus); From 70c2663ef1df9df9ef4a278e6fc48c013e456e72 Mon Sep 17 00:00:00 2001 From: MD YUNUS Date: Sun, 15 Mar 2026 14:43:03 +0530 Subject: [PATCH 11/14] fix(pages-router): add static file check for fallback rewrites --- packages/vinext/src/index.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index a33e88cd2..0122e3f6c 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -2452,6 +2452,32 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { await proxyExternalRewriteNode(req, res, fallbackRewrite); return; } + // Check if fallback targets a static file in public/ + const fallbackPathname = fallbackRewrite.split("?")[0]; + if (path.extname(fallbackPathname)) { + const resolvedPublicDir = path.resolve(root, "public"); + const publicFilePath = path.resolve( + resolvedPublicDir, + "." + fallbackPathname, + ); + if ( + publicFilePath.startsWith(resolvedPublicDir + path.sep) + ) { + try { + const stat = fs.statSync(publicFilePath); + if (stat.isFile()) { + const content = fs.readFileSync(publicFilePath); + const ext = path.extname(fallbackPathname).slice(1).toLowerCase(); + res.writeHead(200, { "Content-Type": mimeType(ext) }); + res.end(content); + return; + } + } catch (e: any) { + if (e?.code !== "ENOENT") + console.warn("[vinext] static file check failed:", e); + } + } + } const fallbackMatch = matchRoute(fallbackRewrite.split("?")[0], routes); if (!fallbackMatch && hasAppDir) { return next(); From 1a1d97b978c9705a6622ca44628053a90b065c7d Mon Sep 17 00:00:00 2001 From: MD YUNUS Date: Sun, 15 Mar 2026 14:43:23 +0530 Subject: [PATCH 12/14] fix: apply oxfmt formatting --- packages/vinext/src/index.ts | 37 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 0122e3f6c..0c3843039 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -2391,22 +2391,20 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { resolvedPublicDir, "." + afterFilesPathname, ); - if ( - publicFilePath.startsWith(resolvedPublicDir + path.sep) - ) { - try { - const stat = fs.statSync(publicFilePath); - if (stat.isFile()) { - const content = fs.readFileSync(publicFilePath); - const ext = path.extname(afterFilesPathname).slice(1).toLowerCase(); - res.writeHead(200, { "Content-Type": mimeType(ext) }); - res.end(content); - return; + if (publicFilePath.startsWith(resolvedPublicDir + path.sep)) { + try { + const stat = fs.statSync(publicFilePath); + if (stat.isFile()) { + const content = fs.readFileSync(publicFilePath); + const ext = path.extname(afterFilesPathname).slice(1).toLowerCase(); + res.writeHead(200, { "Content-Type": mimeType(ext) }); + res.end(content); + return; + } + } catch (e: any) { + if (e?.code !== "ENOENT") + console.warn("[vinext] static file check failed:", e); } - } catch (e: any) { - if (e?.code !== "ENOENT") - console.warn("[vinext] static file check failed:", e); - } } } } @@ -2456,13 +2454,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { const fallbackPathname = fallbackRewrite.split("?")[0]; if (path.extname(fallbackPathname)) { const resolvedPublicDir = path.resolve(root, "public"); - const publicFilePath = path.resolve( - resolvedPublicDir, - "." + fallbackPathname, - ); - if ( - publicFilePath.startsWith(resolvedPublicDir + path.sep) - ) { + const publicFilePath = path.resolve(resolvedPublicDir, "." + fallbackPathname); + if (publicFilePath.startsWith(resolvedPublicDir + path.sep)) { try { const stat = fs.statSync(publicFilePath); if (stat.isFile()) { From aeefb4588124c6e249ece95a5a0a0dcd896b99d4 Mon Sep 17 00:00:00 2001 From: MD YUNUS Date: Sun, 15 Mar 2026 15:03:58 +0530 Subject: [PATCH 13/14] fix(vite-8): remove deprecated Rollup config options for Vite 8/Rolldown --- packages/vinext/src/cli.ts | 12 +++++-- packages/vinext/src/index.ts | 62 +++++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/packages/vinext/src/cli.ts b/packages/vinext/src/cli.ts index 88598bb0a..780e5c999 100644 --- a/packages/vinext/src/cli.ts +++ b/packages/vinext/src/cli.ts @@ -13,7 +13,12 @@ * needed for most Next.js apps. */ -import vinext, { clientOutputConfig, clientTreeshakeConfig } from "./index.js"; +import vinext, { + clientOutputConfig, + clientTreeshakeConfig, + getClientOutputConfig, + getClientTreeshakeConfig, +} from "./index.js"; import { printBuildReport } from "./build/report.js"; import path from "node:path"; import fs from "node:fs"; @@ -350,6 +355,7 @@ async function buildApp() { console.log(`\n vinext build (Vite ${getViteVersion()})\n`); const isApp = hasAppDir(); + const viteMajorVersion = parseInt(getViteVersion().split(".")[0], 10); // In verbose mode, skip the custom logger so raw Vite/Rollup output is shown. const logger = parsed.verbose ? vite.createLogger("info", { allowClearScreen: false }) @@ -387,8 +393,8 @@ async function buildApp() { ssrManifest: true, rollupOptions: { input: "virtual:vinext-client-entry", - output: clientOutputConfig, - treeshake: clientTreeshakeConfig, + output: getClientOutputConfig(viteMajorVersion), + treeshake: getClientTreeshakeConfig(viteMajorVersion), }, }, }, diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 0c3843039..9f0ccf717 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -490,6 +490,9 @@ function clientManualChunks(id: string): string | undefined { * their importers. This reduces HTTP request count and improves gzip * compression efficiency — small files restart the compression dictionary, * adding ~5-15% wire overhead vs fewer larger chunks. + * + * Note: experimentalMinChunkSize is a Rollup-only feature (Vite 7). + * Vite 8+ uses Rolldown which doesn't support this option. */ const clientOutputConfig = { manualChunks: clientManualChunks, @@ -520,12 +523,46 @@ const clientOutputConfig = { * tryCatchDeoptimization: false, which can break specific libraries * that rely on property access side effects or try/catch for feature detection * - 'recommended' + 'no-external' gives most of the benefit with less risk + * + * Note: The `preset` option is Rollup-only (Vite 7). + * Vite 8+ uses Rolldown which has different treeshake options. */ const clientTreeshakeConfig = { preset: "recommended" as const, moduleSideEffects: "no-external" as const, }; +/** + * Get Rollup-compatible output config for client builds. + * Returns config without Vite 8/Rolldown-incompatible options. + */ +function getClientOutputConfig(viteVersion: number) { + if (viteVersion >= 8) { + // Vite 8+ uses Rolldown which doesn't support experimentalMinChunkSize + return { + manualChunks: clientManualChunks, + }; + } + // Vite 7 uses Rollup with experimentalMinChunkSize support + return clientOutputConfig; +} + +/** + * Get Rollup-compatible treeshake config for client builds. + * Returns config without Vite 8/Rolldown-incompatible options. + */ +function getClientTreeshakeConfig(viteVersion: number) { + if (viteVersion >= 8) { + // Vite 8+ uses Rolldown which doesn't support `preset` option + // moduleSideEffects is still supported in Rolldown + return { + moduleSideEffects: "no-external" as const, + }; + } + // Vite 7 uses Rollup with preset support + return clientTreeshakeConfig; +} + type BuildManifestChunk = { file: string; isEntry?: boolean; @@ -1233,7 +1270,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // avoid leaking into RSC/SSR environments where // moduleSideEffects: 'no-external' could drop server packages // that rely on module-level side effects. - ...(!isSSR && !isMultiEnv ? { treeshake: clientTreeshakeConfig } : {}), + ...(!isSSR && !isMultiEnv + ? { treeshake: getClientTreeshakeConfig(viteMajorVersion) } + : {}), // Code-split client bundles: separate framework (React/ReactDOM), // vinext runtime (shims), and vendor packages into their own // chunks so pages only load the JS they need. @@ -1241,7 +1280,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Router). For multi-environment builds (App Router, Cloudflare), // manualChunks is set per-environment on the client env below // to avoid leaking into RSC/SSR environments. - ...(!isSSR && !isMultiEnv ? { output: clientOutputConfig } : {}), + ...(!isSSR && !isMultiEnv ? { output: getClientOutputConfig(viteMajorVersion) } : {}), }, }, // Let OPTIONS requests pass through Vite's CORS middleware to our @@ -1438,15 +1477,15 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // When targeting Cloudflare Workers, enable manifest generation // so the vinext:cloudflare-build closeBundle hook can read the // client build manifest, compute lazy chunks (only reachable - // via dynamic imports), and inject __VINEXT_LAZY_CHUNKS__ into + // via dynamic imports), and inject __VINETT_LAZY_CHUNKS__ into // the worker entry. Without this, all chunks are modulepreloaded // on every page — defeating code-splitting for React.lazy() and // next/dynamic boundaries. ...(hasCloudflarePlugin ? { manifest: true } : {}), rollupOptions: { input: { index: VIRTUAL_APP_BROWSER_ENTRY }, - output: clientOutputConfig, - treeshake: clientTreeshakeConfig, + output: getClientOutputConfig(viteMajorVersion), + treeshake: getClientTreeshakeConfig(viteMajorVersion), }, }, }, @@ -1464,8 +1503,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { ssrManifest: true, rollupOptions: { input: { index: VIRTUAL_CLIENT_ENTRY }, - output: clientOutputConfig, - treeshake: clientTreeshakeConfig, + output: getClientOutputConfig(viteMajorVersion), + treeshake: getClientTreeshakeConfig(viteMajorVersion), }, }, }, @@ -3814,7 +3853,14 @@ export type { export type { NextConfig } from "./config/next-config.js"; // Exported for CLI and testing -export { clientManualChunks, clientOutputConfig, clientTreeshakeConfig, computeLazyChunks }; +export { + clientManualChunks, + clientOutputConfig, + clientTreeshakeConfig, + computeLazyChunks, + getClientOutputConfig, + getClientTreeshakeConfig, +}; export { augmentSsrManifestFromBundle as _augmentSsrManifestFromBundle }; export { resolvePostcssStringPlugins as _resolvePostcssStringPlugins }; export { _postcssCache }; From b16247608f23c96dc0adda18897937dc5eb5f173 Mon Sep 17 00:00:00 2001 From: MD YUNUS Date: Sun, 15 Mar 2026 15:06:08 +0530 Subject: [PATCH 14/14] Revert "fix(vite-8): remove deprecated Rollup config options for Vite 8/Rolldown" This reverts commit aeefb4588124c6e249ece95a5a0a0dcd896b99d4. --- packages/vinext/src/cli.ts | 12 ++----- packages/vinext/src/index.ts | 62 +++++------------------------------- 2 files changed, 11 insertions(+), 63 deletions(-) diff --git a/packages/vinext/src/cli.ts b/packages/vinext/src/cli.ts index 780e5c999..88598bb0a 100644 --- a/packages/vinext/src/cli.ts +++ b/packages/vinext/src/cli.ts @@ -13,12 +13,7 @@ * needed for most Next.js apps. */ -import vinext, { - clientOutputConfig, - clientTreeshakeConfig, - getClientOutputConfig, - getClientTreeshakeConfig, -} from "./index.js"; +import vinext, { clientOutputConfig, clientTreeshakeConfig } from "./index.js"; import { printBuildReport } from "./build/report.js"; import path from "node:path"; import fs from "node:fs"; @@ -355,7 +350,6 @@ async function buildApp() { console.log(`\n vinext build (Vite ${getViteVersion()})\n`); const isApp = hasAppDir(); - const viteMajorVersion = parseInt(getViteVersion().split(".")[0], 10); // In verbose mode, skip the custom logger so raw Vite/Rollup output is shown. const logger = parsed.verbose ? vite.createLogger("info", { allowClearScreen: false }) @@ -393,8 +387,8 @@ async function buildApp() { ssrManifest: true, rollupOptions: { input: "virtual:vinext-client-entry", - output: getClientOutputConfig(viteMajorVersion), - treeshake: getClientTreeshakeConfig(viteMajorVersion), + output: clientOutputConfig, + treeshake: clientTreeshakeConfig, }, }, }, diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 9f0ccf717..0c3843039 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -490,9 +490,6 @@ function clientManualChunks(id: string): string | undefined { * their importers. This reduces HTTP request count and improves gzip * compression efficiency — small files restart the compression dictionary, * adding ~5-15% wire overhead vs fewer larger chunks. - * - * Note: experimentalMinChunkSize is a Rollup-only feature (Vite 7). - * Vite 8+ uses Rolldown which doesn't support this option. */ const clientOutputConfig = { manualChunks: clientManualChunks, @@ -523,46 +520,12 @@ const clientOutputConfig = { * tryCatchDeoptimization: false, which can break specific libraries * that rely on property access side effects or try/catch for feature detection * - 'recommended' + 'no-external' gives most of the benefit with less risk - * - * Note: The `preset` option is Rollup-only (Vite 7). - * Vite 8+ uses Rolldown which has different treeshake options. */ const clientTreeshakeConfig = { preset: "recommended" as const, moduleSideEffects: "no-external" as const, }; -/** - * Get Rollup-compatible output config for client builds. - * Returns config without Vite 8/Rolldown-incompatible options. - */ -function getClientOutputConfig(viteVersion: number) { - if (viteVersion >= 8) { - // Vite 8+ uses Rolldown which doesn't support experimentalMinChunkSize - return { - manualChunks: clientManualChunks, - }; - } - // Vite 7 uses Rollup with experimentalMinChunkSize support - return clientOutputConfig; -} - -/** - * Get Rollup-compatible treeshake config for client builds. - * Returns config without Vite 8/Rolldown-incompatible options. - */ -function getClientTreeshakeConfig(viteVersion: number) { - if (viteVersion >= 8) { - // Vite 8+ uses Rolldown which doesn't support `preset` option - // moduleSideEffects is still supported in Rolldown - return { - moduleSideEffects: "no-external" as const, - }; - } - // Vite 7 uses Rollup with preset support - return clientTreeshakeConfig; -} - type BuildManifestChunk = { file: string; isEntry?: boolean; @@ -1270,9 +1233,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // avoid leaking into RSC/SSR environments where // moduleSideEffects: 'no-external' could drop server packages // that rely on module-level side effects. - ...(!isSSR && !isMultiEnv - ? { treeshake: getClientTreeshakeConfig(viteMajorVersion) } - : {}), + ...(!isSSR && !isMultiEnv ? { treeshake: clientTreeshakeConfig } : {}), // Code-split client bundles: separate framework (React/ReactDOM), // vinext runtime (shims), and vendor packages into their own // chunks so pages only load the JS they need. @@ -1280,7 +1241,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Router). For multi-environment builds (App Router, Cloudflare), // manualChunks is set per-environment on the client env below // to avoid leaking into RSC/SSR environments. - ...(!isSSR && !isMultiEnv ? { output: getClientOutputConfig(viteMajorVersion) } : {}), + ...(!isSSR && !isMultiEnv ? { output: clientOutputConfig } : {}), }, }, // Let OPTIONS requests pass through Vite's CORS middleware to our @@ -1477,15 +1438,15 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // When targeting Cloudflare Workers, enable manifest generation // so the vinext:cloudflare-build closeBundle hook can read the // client build manifest, compute lazy chunks (only reachable - // via dynamic imports), and inject __VINETT_LAZY_CHUNKS__ into + // via dynamic imports), and inject __VINEXT_LAZY_CHUNKS__ into // the worker entry. Without this, all chunks are modulepreloaded // on every page — defeating code-splitting for React.lazy() and // next/dynamic boundaries. ...(hasCloudflarePlugin ? { manifest: true } : {}), rollupOptions: { input: { index: VIRTUAL_APP_BROWSER_ENTRY }, - output: getClientOutputConfig(viteMajorVersion), - treeshake: getClientTreeshakeConfig(viteMajorVersion), + output: clientOutputConfig, + treeshake: clientTreeshakeConfig, }, }, }, @@ -1503,8 +1464,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { ssrManifest: true, rollupOptions: { input: { index: VIRTUAL_CLIENT_ENTRY }, - output: getClientOutputConfig(viteMajorVersion), - treeshake: getClientTreeshakeConfig(viteMajorVersion), + output: clientOutputConfig, + treeshake: clientTreeshakeConfig, }, }, }, @@ -3853,14 +3814,7 @@ export type { export type { NextConfig } from "./config/next-config.js"; // Exported for CLI and testing -export { - clientManualChunks, - clientOutputConfig, - clientTreeshakeConfig, - computeLazyChunks, - getClientOutputConfig, - getClientTreeshakeConfig, -}; +export { clientManualChunks, clientOutputConfig, clientTreeshakeConfig, computeLazyChunks }; export { augmentSsrManifestFromBundle as _augmentSsrManifestFromBundle }; export { resolvePostcssStringPlugins as _resolvePostcssStringPlugins }; export { _postcssCache };