diff --git a/.changeset/light-moose-attend.md b/.changeset/light-moose-attend.md new file mode 100644 index 000000000000..0b32eba8f26b --- /dev/null +++ b/.changeset/light-moose-attend.md @@ -0,0 +1,5 @@ +--- +'astro': minor +--- + +Add `server.headers` option diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index c2011926c3da..9e5fc377b219 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -9,6 +9,7 @@ import { fileURLToPath } from 'url'; import { z } from 'zod'; import { appendForwardSlash, prependForwardSlash, trimSlashes } from '../path.js'; import { isObject } from '../util.js'; +import { OutgoingHttpHeaders } from 'http'; const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = { root: '.', @@ -125,6 +126,7 @@ export const AstroConfigSchema = z.object({ .optional() .default(ASTRO_CONFIG_DEFAULTS.server.host), port: z.number().optional().default(ASTRO_CONFIG_DEFAULTS.server.port), + headers: z.custom().optional(), }) .optional() .default({}) @@ -287,6 +289,7 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) { .optional() .default(ASTRO_CONFIG_DEFAULTS.server.host), port: z.number().optional().default(ASTRO_CONFIG_DEFAULTS.server.port), + headers: z.custom().optional(), streaming: z.boolean().optional().default(true), }) .optional() diff --git a/packages/astro/src/core/dev/container.ts b/packages/astro/src/core/dev/container.ts index c9eaaa8eb50d..87cd2b7ad33a 100644 --- a/packages/astro/src/core/dev/container.ts +++ b/packages/astro/src/core/dev/container.ts @@ -65,7 +65,7 @@ export async function createContainer(params: CreateContainerParams = {}): Promi logging, isRestart, }); - const { host } = settings.config.server; + const { host, headers } = settings.config.server; // The client entrypoint for renderers. Since these are imported dynamically // we need to tell Vite to preoptimize them. @@ -76,7 +76,7 @@ export async function createContainer(params: CreateContainerParams = {}): Promi const viteConfig = await createVite( { mode: 'development', - server: { host }, + server: { host, headers }, optimizeDeps: { include: rendererClientEntries, }, diff --git a/packages/astro/src/core/preview/index.ts b/packages/astro/src/core/preview/index.ts index ea8eef1e1a07..d4a5c8bb2cb5 100644 --- a/packages/astro/src/core/preview/index.ts +++ b/packages/astro/src/core/preview/index.ts @@ -24,10 +24,10 @@ export default async function preview( }); await runHookConfigDone({ settings: settings, logging: logging }); const host = getResolvedHostForHttpServer(settings.config.server.host); - const { port } = settings.config.server; + const { port, headers } = settings.config.server; if (settings.config.output === 'static') { - const server = await createStaticPreviewServer(settings, { logging, host, port }); + const server = await createStaticPreviewServer(settings, { logging, host, port, headers }); return server; } if (!settings.adapter) { diff --git a/packages/astro/src/core/preview/static-preview-server.ts b/packages/astro/src/core/preview/static-preview-server.ts index 942567029361..f74755aed550 100644 --- a/packages/astro/src/core/preview/static-preview-server.ts +++ b/packages/astro/src/core/preview/static-preview-server.ts @@ -3,7 +3,7 @@ import type { AstroSettings } from '../../@types/astro'; import type { LogOptions } from '../logger/core'; import fs from 'fs'; -import http from 'http'; +import http, { OutgoingHttpHeaders } from 'http'; import { performance } from 'perf_hooks'; import sirv from 'sirv'; import { fileURLToPath } from 'url'; @@ -24,7 +24,17 @@ const HAS_FILE_EXTENSION_REGEXP = /^.*\.[^\\]+$/; /** The primary dev action */ export default async function createStaticPreviewServer( settings: AstroSettings, - { logging, host, port }: { logging: LogOptions; host: string | undefined; port: number } + { + logging, + host, + port, + headers, + }: { + logging: LogOptions; + host: string | undefined; + port: number; + headers: OutgoingHttpHeaders | undefined; + } ): Promise { const startServerTime = performance.now(); const defaultOrigin = 'http://localhost'; @@ -35,6 +45,11 @@ export default async function createStaticPreviewServer( dev: true, etag: true, maxAge: 0, + setHeaders: (res, pathname, stats) => { + for (const [name, value] of Object.entries(headers ?? {})) { + if (value) res.setHeader(name, value); + } + }, }); // Create the preview server, send static files out of the `dist/` directory. const server = http.createServer((req, res) => { diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index 9cd4ab239aa1..5bd551ca9c20 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -144,6 +144,11 @@ export async function handleRoute( clientAddress: buildingToSSR ? req.socket.remoteAddress : undefined, }); + // Set user specified headers to response object. + for (const [name, value] of Object.entries(config.server.headers ?? {})) { + if (value) res.setHeader(name, value); + } + // attempt to get static paths // if this fails, we have a bad URL match! const paramsAndPropsRes = await getParamsAndProps({ diff --git a/packages/astro/test/astro-dev-headers.test.js b/packages/astro/test/astro-dev-headers.test.js new file mode 100644 index 000000000000..7cc2266c65b6 --- /dev/null +++ b/packages/astro/test/astro-dev-headers.test.js @@ -0,0 +1,39 @@ +import { expect } from 'chai'; +import { loadFixture } from './test-utils.js'; + +describe('Astro dev headers', () => { + let fixture; + let devServer; + const headers = { + 'x-astro': 'test', + }; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/astro-dev-headers/', + server: { + headers, + }, + }); + await fixture.build(); + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + describe('dev', () => { + it('returns custom headers for valid URLs', async () => { + const result = await fixture.fetch('/'); + expect(result.status).to.equal(200); + expect(Object.fromEntries(result.headers)).to.include(headers); + }); + + it('does not return custom headers for invalid URLs', async () => { + const result = await fixture.fetch('/bad-url'); + expect(result.status).to.equal(404); + expect(Object.fromEntries(result.headers)).not.to.include(headers); + }); + }); +}); diff --git a/packages/astro/test/astro-preview-headers.test.js b/packages/astro/test/astro-preview-headers.test.js new file mode 100644 index 000000000000..ef05399de60c --- /dev/null +++ b/packages/astro/test/astro-preview-headers.test.js @@ -0,0 +1,40 @@ +import { expect } from 'chai'; +import { loadFixture } from './test-utils.js'; + +describe('Astro preview headers', () => { + let fixture; + let previewServer; + const headers = { + astro: 'test', + }; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/astro-preview-headers/', + server: { + headers, + }, + }); + await fixture.build(); + previewServer = await fixture.preview(); + }); + + // important: close preview server (free up port and connection) + after(async () => { + await previewServer.stop(); + }); + + describe('preview', () => { + it('returns custom headers for valid URLs', async () => { + const result = await fixture.fetch('/'); + expect(result.status).to.equal(200); + expect(Object.fromEntries(result.headers)).to.include(headers); + }); + + it('does not return custom headers for invalid URLs', async () => { + const result = await fixture.fetch('/bad-url'); + expect(result.status).to.equal(404); + expect(Object.fromEntries(result.headers)).not.to.include(headers); + }); + }); +}); diff --git a/packages/astro/test/fixtures/astro-dev-headers/package.json b/packages/astro/test/fixtures/astro-dev-headers/package.json new file mode 100644 index 000000000000..891529f80eb3 --- /dev/null +++ b/packages/astro/test/fixtures/astro-dev-headers/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/astro-dev-headers", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/astro-dev-headers/src/pages/index.astro b/packages/astro/test/fixtures/astro-dev-headers/src/pages/index.astro new file mode 100644 index 000000000000..47d2f7fc6561 --- /dev/null +++ b/packages/astro/test/fixtures/astro-dev-headers/src/pages/index.astro @@ -0,0 +1,8 @@ + + + + +

Hello world!

+ + + diff --git a/packages/astro/test/fixtures/astro-preview-headers/package.json b/packages/astro/test/fixtures/astro-preview-headers/package.json new file mode 100644 index 000000000000..23d664eb4945 --- /dev/null +++ b/packages/astro/test/fixtures/astro-preview-headers/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/astro-preview-headers", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/astro-preview-headers/src/pages/index.astro b/packages/astro/test/fixtures/astro-preview-headers/src/pages/index.astro new file mode 100644 index 000000000000..47d2f7fc6561 --- /dev/null +++ b/packages/astro/test/fixtures/astro-preview-headers/src/pages/index.astro @@ -0,0 +1,8 @@ + + + + +

Hello world!

+ + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3eb668973a1..c895ce12879e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1288,6 +1288,12 @@ importers: dependencies: astro: link:../../.. + packages/astro/test/fixtures/astro-dev-headers: + specifiers: + astro: workspace:* + dependencies: + astro: link:../../.. + packages/astro/test/fixtures/astro-directives: specifiers: astro: workspace:* @@ -1514,6 +1520,12 @@ importers: react: 18.2.0 react-dom: 18.2.0_react@18.2.0 + packages/astro/test/fixtures/astro-preview-headers: + specifiers: + astro: workspace:* + dependencies: + astro: link:../../.. + packages/astro/test/fixtures/astro-public: specifiers: astro: workspace:*