diff --git a/src/build/vite/dev.ts b/src/build/vite/dev.ts index 04d1f0fd89..747e274a9c 100644 --- a/src/build/vite/dev.ts +++ b/src/build/vite/dev.ts @@ -11,6 +11,7 @@ import { watch as chokidarWatch } from "chokidar"; import { watch as fsWatch } from "node:fs"; import { join } from "pathe"; import { debounce } from "perfect-debounce"; +import { withBase } from "ufo"; import { scanHandlers } from "../../scan.ts"; import { getEnvRunner } from "./env.ts"; @@ -198,6 +199,12 @@ export async function configureViteDevServer(ctx: NitroPluginContext, server: Vi return next(); } nodeReq._nitroHandled = true; + + const baseURL = nitro.options.baseURL || "/"; + const originalURL = nodeReq.url; + if (baseURL !== "/") { + nodeReq.url = withBase(nodeReq.url, baseURL); + } try { // Create web API compat request const req = new NodeRequest({ req: nodeReq, res: nodeRes }); @@ -219,6 +226,10 @@ export async function configureViteDevServer(ctx: NitroPluginContext, server: Vi return await sendNodeResponse(nodeRes, envRes); } catch (error) { return next(error); + } finally { + if (baseURL !== "/") { + nodeReq.url = originalURL; + } } }; @@ -226,12 +237,19 @@ export async function configureViteDevServer(ctx: NitroPluginContext, server: Vi // https://github.com/vitejs/vite/pull/20866 server.middlewares.use(function nitroDevMiddlewarePre(req, res, next) { const fetchDest = req.headers["sec-fetch-dest"]; + const ext = req.url!.match(/\.([a-z0-9]+)(?:[?#]|$)/i)?.[1]; + const isNitroRoute = ext + ? !!nitro.routing.routes.match( + req.method || "", + new URL(withBase(req.url!, nitro.options.baseURL), "http://localhost").pathname + ) + : false; res.setHeader("vary", "sec-fetch-dest"); if ( // Originating from browser tab or no fetch dest (curl, fetch, etc) and (not script, style, image, etc) (!fetchDest || /^(document|iframe|frame|empty)$/.test(fetchDest)) && - // No file extension (not /src/index.ts) - !req.url!.match(/\.([a-z0-9]+)(?:[?#]|$)/i)?.[1] && + // No file extension (not /src/index.ts) unless it is an explicit Nitro route + (!ext || isNitroRoute) && // Special prefixes (/__vue-router/auto-routes, /@vite-plugin-layouts/, etc) !/^\/(?:__|@)/.test(req.url!) ) { diff --git a/test/vite/baseurl-dotted-param-fixture/api/proxy/[...param].ts b/test/vite/baseurl-dotted-param-fixture/api/proxy/[...param].ts new file mode 100644 index 0000000000..98672e7806 --- /dev/null +++ b/test/vite/baseurl-dotted-param-fixture/api/proxy/[...param].ts @@ -0,0 +1 @@ +export default (event: any) => event.context.params!.param; diff --git a/test/vite/baseurl-dotted-param-fixture/index.html b/test/vite/baseurl-dotted-param-fixture/index.html new file mode 100644 index 0000000000..5a55be9846 --- /dev/null +++ b/test/vite/baseurl-dotted-param-fixture/index.html @@ -0,0 +1,11 @@ + + + + + + Nitro Base URL Dotted Param Fixture + + +
fixture
+ + diff --git a/test/vite/baseurl-dotted-param-fixture/tsconfig.json b/test/vite/baseurl-dotted-param-fixture/tsconfig.json new file mode 100644 index 0000000000..4b886bd47e --- /dev/null +++ b/test/vite/baseurl-dotted-param-fixture/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "nitro/tsconfig" +} diff --git a/test/vite/baseurl-dotted-param-fixture/vite.config.ts b/test/vite/baseurl-dotted-param-fixture/vite.config.ts new file mode 100644 index 0000000000..23f6a51841 --- /dev/null +++ b/test/vite/baseurl-dotted-param-fixture/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "vite"; +import { nitro } from "nitro/vite"; + +export default defineConfig({ + base: "/subdir/", + plugins: [ + nitro({ + baseURL: "/subdir/", + serverDir: "./", + serveStatic: false, + }), + ], +}); diff --git a/test/vite/baseurl-dotted-param.test.ts b/test/vite/baseurl-dotted-param.test.ts new file mode 100644 index 0000000000..e1cc39cd8a --- /dev/null +++ b/test/vite/baseurl-dotted-param.test.ts @@ -0,0 +1,49 @@ +import { fileURLToPath } from "node:url"; +import type { ViteDevServer } from "vite"; +import { beforeAll, afterAll, describe, expect, test } from "vitest"; + +const { createServer } = (await import( + process.env.NITRO_VITE_PKG || "vite" +)) as typeof import("vite"); + +describe("vite:baseURL dotted params", { sequential: true }, () => { + let server: ViteDevServer; + let serverURL: string; + + const rootDir = fileURLToPath(new URL("./baseurl-dotted-param-fixture", import.meta.url)); + + beforeAll(async () => { + process.chdir(rootDir); + server = await createServer({ root: rootDir }); + await server.listen("0" as unknown as number); + const addr = server.httpServer?.address() as { + port: number; + address: string; + family: string; + }; + serverURL = `http://${addr.family === "IPv6" ? `[${addr.address}]` : addr.address}:${addr.port}`; + }, 30_000); + + afterAll(async () => { + await server?.close(); + }); + + test("serves Nitro API routes with dotted params under baseURL without redirecting", async () => { + for (const fetchDest of ["empty", "document", undefined]) { + const headers: Record = {}; + if (fetchDest) { + headers["sec-fetch-dest"] = fetchDest; + } + const response = await fetch(`${serverURL}/subdir/api/proxy/todos/Package.todos.Entity.3`, { + headers, + redirect: "manual", + }); + + expect(response.status, `sec-fetch-dest: ${fetchDest}`).toBe(200); + expect(response.headers.get("location"), `sec-fetch-dest: ${fetchDest}`).toBeNull(); + expect(await response.text(), `sec-fetch-dest: ${fetchDest}`).toBe( + "todos/Package.todos.Entity.3" + ); + } + }); +});