diff --git a/src/core/dev-server/server.ts b/src/core/dev-server/server.ts index 7fd90cef37..0007726020 100644 --- a/src/core/dev-server/server.ts +++ b/src/core/dev-server/server.ts @@ -22,6 +22,7 @@ import type { Nitro, NitroBuildInfo, NitroDevServer, + NitroRouteRules, NitroWorker, } from "nitropack/types"; import { resolve } from "pathe"; @@ -31,6 +32,8 @@ import serveStatic from "serve-static"; import { joinURL } from "ufo"; import defaultErrorHandler from "./error"; import { createVFSHandler } from "./vfs"; +import { createRouter as createRadixRouter, toRouteMatcher } from "radix3"; +import defu from "defu"; function initWorker(filename: string): Promise | undefined { if (!existsSync(filename)) { @@ -166,13 +169,40 @@ export function createDevServer(nitro: Nitro): NitroDevServer { // Debugging endpoint to view vfs app.use("/_vfs", createVFSHandler(nitro)); + // Apply headers based on routeRules. In cases where the dev server is being + // used, these would otherwise fail to be applied to public assets. + const routeRulesMatcher = toRouteMatcher( + createRadixRouter({ routes: nitro.options.routeRules }) + ); // Serve asset dirs for (const asset of nitro.options.publicAssets) { const url = joinURL( nitro.options.runtimeConfig.app.baseURL, asset.baseURL || "/" ); - app.use(url, fromNodeMiddleware(serveStatic(asset.dir))); + app.use( + url, + fromNodeMiddleware( + serveStatic(asset.dir, { + setHeaders: (res) => { + const path = res.req.url; + if (path === undefined) { + return; + } + const rules: NitroRouteRules = defu( + {}, + ...routeRulesMatcher.matchAll(path).reverse() + ); + if (!rules.headers) { + return; + } + for (const [k, v] of Object.entries(rules.headers)) { + res.appendHeader(k, v); + } + }, + }) + ) + ); if (!asset.fallthrough) { app.use(url, fromNodeMiddleware(servePlaceholder())); } diff --git a/test/fixture/nitro.config.ts b/test/fixture/nitro.config.ts index 63d8eca4cd..1d0a985530 100644 --- a/test/fixture/nitro.config.ts +++ b/test/fixture/nitro.config.ts @@ -91,6 +91,7 @@ export default defineNitroConfig({ "/rules/_/cached/noncached": { cache: false, swr: false, isr: false }, "/rules/_/cached/**": { swr: true }, "/api/proxy/**": { proxy: "/api/echo" }, + "/build/**": { headers: { "x-build-header": "works" } }, }, prerender: { crawlLinks: true, diff --git a/test/presets/netlify-legacy.test.ts b/test/presets/netlify-legacy.test.ts index 5de5771d7f..33e8ba4733 100644 --- a/test/presets/netlify-legacy.test.ts +++ b/test/presets/netlify-legacy.test.ts @@ -82,19 +82,20 @@ describe("nitro:preset:netlify-legacy", async () => { ); expect(headers).toMatchInlineSnapshot(` - "/rules/headers - cache-control: s-maxage=60 - /rules/cors - access-control-allow-origin: * - access-control-allow-methods: GET - access-control-allow-headers: * - access-control-max-age: 0 - /rules/nested/* - x-test: test - /build/* - cache-control: public, max-age=3600, immutable - " - `); + "/rules/headers + cache-control: s-maxage=60 + /rules/cors + access-control-allow-origin: * + access-control-allow-methods: GET + access-control-allow-headers: * + access-control-max-age: 0 + /rules/nested/* + x-test: test + /build/* + cache-control: public, max-age=3600, immutable + x-build-header: works + " + `); }); it("should write config.json", async () => { const config = await fsp diff --git a/test/presets/netlify.test.ts b/test/presets/netlify.test.ts index a74afb093b..54dc76705a 100644 --- a/test/presets/netlify.test.ts +++ b/test/presets/netlify.test.ts @@ -62,19 +62,20 @@ describe("nitro:preset:netlify", async () => { ); expect(headers).toMatchInlineSnapshot(` - "/rules/headers - cache-control: s-maxage=60 - /rules/cors - access-control-allow-origin: * - access-control-allow-methods: GET - access-control-allow-headers: * - access-control-max-age: 0 - /rules/nested/* - x-test: test - /build/* - cache-control: public, max-age=3600, immutable - " - `); + "/rules/headers + cache-control: s-maxage=60 + /rules/cors + access-control-allow-origin: * + access-control-allow-methods: GET + access-control-allow-headers: * + access-control-max-age: 0 + /rules/nested/* + x-test: test + /build/* + cache-control: public, max-age=3600, immutable + x-build-header: works + " + `); }); it("writes config.json", async () => { diff --git a/test/presets/nitro-dev.test.ts b/test/presets/nitro-dev.test.ts index 9c6c39c5b7..cb8aa2a260 100644 --- a/test/presets/nitro-dev.test.ts +++ b/test/presets/nitro-dev.test.ts @@ -28,6 +28,22 @@ describe("nitro:preset:nitro-dev", async () => { expect(data.keys).includes("src:nitro.config.ts"); }); + it("static asset headers", async () => { + const { headers } = await ctx.fetch("/build/test.txt"); + expect(Object.fromEntries(headers)).toMatchObject({ + "accept-ranges": "bytes", + "cache-control": "public, max-age=0", + "last-modified": expect.any(String), + etag: 'W/"7-18df5a508c5"', + "content-type": "text/plain; charset=UTF-8", + "content-length": "7", + date: expect.any(String), + connection: "keep-alive", + "keep-alive": "timeout=5", + "x-build-header": "works", + }); + }); + describe("openAPI", () => { let spec: OpenAPI3; it("/_openapi.json", async () => { diff --git a/test/presets/vercel.test.ts b/test/presets/vercel.test.ts index 25ad76cfe2..902d806106 100644 --- a/test/presets/vercel.test.ts +++ b/test/presets/vercel.test.ts @@ -93,6 +93,7 @@ describe("nitro:preset:vercel", async () => { { "headers": { "cache-control": "public, max-age=3600, immutable", + "x-build-header": "works", }, "src": "/build/(.*)", },