Add server.headers
option (#5564)
With this new `server.headers` option, the users can specify custom headers for `astro dev` and `astro preview` servers. This is useful when they want to build a website requiring specific response headers such as `Cross-Origin-Opener-Policy`.
This commit is contained in:
parent
8913c51e1a
commit
dced4a8a26
13 changed files with 157 additions and 6 deletions
5
.changeset/light-moose-attend.md
Normal file
5
.changeset/light-moose-attend.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add `server.headers` option
|
|
@ -9,6 +9,7 @@ import { fileURLToPath } from 'url';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { appendForwardSlash, prependForwardSlash, trimSlashes } from '../path.js';
|
import { appendForwardSlash, prependForwardSlash, trimSlashes } from '../path.js';
|
||||||
import { isObject } from '../util.js';
|
import { isObject } from '../util.js';
|
||||||
|
import { OutgoingHttpHeaders } from 'http';
|
||||||
|
|
||||||
const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
|
const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
|
||||||
root: '.',
|
root: '.',
|
||||||
|
@ -125,6 +126,7 @@ export const AstroConfigSchema = z.object({
|
||||||
.optional()
|
.optional()
|
||||||
.default(ASTRO_CONFIG_DEFAULTS.server.host),
|
.default(ASTRO_CONFIG_DEFAULTS.server.host),
|
||||||
port: z.number().optional().default(ASTRO_CONFIG_DEFAULTS.server.port),
|
port: z.number().optional().default(ASTRO_CONFIG_DEFAULTS.server.port),
|
||||||
|
headers: z.custom<OutgoingHttpHeaders>().optional(),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({})
|
.default({})
|
||||||
|
@ -287,6 +289,7 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: URL) {
|
||||||
.optional()
|
.optional()
|
||||||
.default(ASTRO_CONFIG_DEFAULTS.server.host),
|
.default(ASTRO_CONFIG_DEFAULTS.server.host),
|
||||||
port: z.number().optional().default(ASTRO_CONFIG_DEFAULTS.server.port),
|
port: z.number().optional().default(ASTRO_CONFIG_DEFAULTS.server.port),
|
||||||
|
headers: z.custom<OutgoingHttpHeaders>().optional(),
|
||||||
streaming: z.boolean().optional().default(true),
|
streaming: z.boolean().optional().default(true),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
|
|
|
@ -65,7 +65,7 @@ export async function createContainer(params: CreateContainerParams = {}): Promi
|
||||||
logging,
|
logging,
|
||||||
isRestart,
|
isRestart,
|
||||||
});
|
});
|
||||||
const { host } = settings.config.server;
|
const { host, headers } = settings.config.server;
|
||||||
|
|
||||||
// The client entrypoint for renderers. Since these are imported dynamically
|
// The client entrypoint for renderers. Since these are imported dynamically
|
||||||
// we need to tell Vite to preoptimize them.
|
// we need to tell Vite to preoptimize them.
|
||||||
|
@ -76,7 +76,7 @@ export async function createContainer(params: CreateContainerParams = {}): Promi
|
||||||
const viteConfig = await createVite(
|
const viteConfig = await createVite(
|
||||||
{
|
{
|
||||||
mode: 'development',
|
mode: 'development',
|
||||||
server: { host },
|
server: { host, headers },
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: rendererClientEntries,
|
include: rendererClientEntries,
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,10 +24,10 @@ export default async function preview(
|
||||||
});
|
});
|
||||||
await runHookConfigDone({ settings: settings, logging: logging });
|
await runHookConfigDone({ settings: settings, logging: logging });
|
||||||
const host = getResolvedHostForHttpServer(settings.config.server.host);
|
const host = getResolvedHostForHttpServer(settings.config.server.host);
|
||||||
const { port } = settings.config.server;
|
const { port, headers } = settings.config.server;
|
||||||
|
|
||||||
if (settings.config.output === 'static') {
|
if (settings.config.output === 'static') {
|
||||||
const server = await createStaticPreviewServer(settings, { logging, host, port });
|
const server = await createStaticPreviewServer(settings, { logging, host, port, headers });
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
if (!settings.adapter) {
|
if (!settings.adapter) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { AstroSettings } from '../../@types/astro';
|
||||||
import type { LogOptions } from '../logger/core';
|
import type { LogOptions } from '../logger/core';
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import http from 'http';
|
import http, { OutgoingHttpHeaders } from 'http';
|
||||||
import { performance } from 'perf_hooks';
|
import { performance } from 'perf_hooks';
|
||||||
import sirv from 'sirv';
|
import sirv from 'sirv';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
@ -24,7 +24,17 @@ const HAS_FILE_EXTENSION_REGEXP = /^.*\.[^\\]+$/;
|
||||||
/** The primary dev action */
|
/** The primary dev action */
|
||||||
export default async function createStaticPreviewServer(
|
export default async function createStaticPreviewServer(
|
||||||
settings: AstroSettings,
|
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<PreviewServer> {
|
): Promise<PreviewServer> {
|
||||||
const startServerTime = performance.now();
|
const startServerTime = performance.now();
|
||||||
const defaultOrigin = 'http://localhost';
|
const defaultOrigin = 'http://localhost';
|
||||||
|
@ -35,6 +45,11 @@ export default async function createStaticPreviewServer(
|
||||||
dev: true,
|
dev: true,
|
||||||
etag: true,
|
etag: true,
|
||||||
maxAge: 0,
|
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.
|
// Create the preview server, send static files out of the `dist/` directory.
|
||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
|
|
|
@ -144,6 +144,11 @@ export async function handleRoute(
|
||||||
clientAddress: buildingToSSR ? req.socket.remoteAddress : undefined,
|
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
|
// attempt to get static paths
|
||||||
// if this fails, we have a bad URL match!
|
// if this fails, we have a bad URL match!
|
||||||
const paramsAndPropsRes = await getParamsAndProps({
|
const paramsAndPropsRes = await getParamsAndProps({
|
||||||
|
|
39
packages/astro/test/astro-dev-headers.test.js
Normal file
39
packages/astro/test/astro-dev-headers.test.js
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
40
packages/astro/test/astro-preview-headers.test.js
Normal file
40
packages/astro/test/astro-preview-headers.test.js
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
8
packages/astro/test/fixtures/astro-dev-headers/package.json
vendored
Normal file
8
packages/astro/test/fixtures/astro-dev-headers/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "@test/astro-dev-headers",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
8
packages/astro/test/fixtures/astro-dev-headers/src/pages/index.astro
vendored
Normal file
8
packages/astro/test/fixtures/astro-dev-headers/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello world!</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
8
packages/astro/test/fixtures/astro-preview-headers/package.json
vendored
Normal file
8
packages/astro/test/fixtures/astro-preview-headers/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "@test/astro-preview-headers",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
8
packages/astro/test/fixtures/astro-preview-headers/src/pages/index.astro
vendored
Normal file
8
packages/astro/test/fixtures/astro-preview-headers/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello world!</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
@ -1288,6 +1288,12 @@ importers:
|
||||||
dependencies:
|
dependencies:
|
||||||
astro: link:../../..
|
astro: link:../../..
|
||||||
|
|
||||||
|
packages/astro/test/fixtures/astro-dev-headers:
|
||||||
|
specifiers:
|
||||||
|
astro: workspace:*
|
||||||
|
dependencies:
|
||||||
|
astro: link:../../..
|
||||||
|
|
||||||
packages/astro/test/fixtures/astro-directives:
|
packages/astro/test/fixtures/astro-directives:
|
||||||
specifiers:
|
specifiers:
|
||||||
astro: workspace:*
|
astro: workspace:*
|
||||||
|
@ -1514,6 +1520,12 @@ importers:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0_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:
|
packages/astro/test/fixtures/astro-public:
|
||||||
specifiers:
|
specifiers:
|
||||||
astro: workspace:*
|
astro: workspace:*
|
||||||
|
|
Loading…
Reference in a new issue