Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion source/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export declare interface Header {
source: string;
headers: {
key: string;
value: string;
value: string | null;
}[];
}

Expand Down Expand Up @@ -78,6 +78,7 @@ export declare interface Options {
'--no-request-logging': boolean;
'--no-clipboard': boolean;
'--no-compression': boolean;
'--no-cache': boolean;
'--no-etag': boolean;
'--symlinks': boolean;
'--cors': boolean;
Expand Down
3 changes: 3 additions & 0 deletions source/utilities/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ const helpText = chalkTemplate`

-u, --no-compression Do not compress files

--no-cache Disable browser caching

--no-etag Send \`Last-Modified\` header instead of \`ETag\`

-S, --symlinks Resolve symlinks instead of showing 404 errors
Expand Down Expand Up @@ -152,6 +154,7 @@ const options = {
'--config': String,
'--no-clipboard': Boolean,
'--no-compression': Boolean,
'--no-cache': Boolean,
'--no-etag': Boolean,
'--symlinks': Boolean,
'--cors': Boolean,
Expand Down
39 changes: 37 additions & 2 deletions source/utilities/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,37 @@ import schema from '@zeit/schemas/deployment/config-static.js';
import { resolve } from './promise.js';
import { logger } from './logger.js';
import type { ErrorObject } from 'ajv';
import type { Configuration, Options, NodeError } from '../types.js';
import type { Configuration, Header, Options, NodeError } from '../types.js';

const noCacheHeader: Header = {
source: '**',
headers: [
{
key: 'Cache-Control',
value: 'no-store, no-cache, must-revalidate, proxy-revalidate',
},
{
key: 'Pragma',
value: 'no-cache',
},
{
key: 'Expires',
value: '0',
},
{
key: 'Surrogate-Control',
value: 'no-store',
},
{
key: 'ETag',
value: null,
},
{
key: 'Last-Modified',
value: null,
},
],
};

/**
* Parses and returns a configuration object from the designated locations.
Expand Down Expand Up @@ -137,8 +167,13 @@ export const loadConfiguration = async (
}

// Configure defaults based on the options the user has passed.
config.etag = !args['--no-etag'];
config.etag = !args['--no-etag'] && !args['--no-cache'];
config.symlinks = args['--symlinks'] || config.symlinks;

if (args['--no-cache']) {
const headers = Array.isArray(config.headers) ? config.headers : [];
config.headers = [...headers, noCacheHeader];
}

return config;
};
4 changes: 3 additions & 1 deletion source/utilities/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ export const startServer = async (
await compress(request as ExpressRequest, response as ExpressResponse);

// Let the `serve-handler` module do the rest.
await handler(request, response, config);
// `serve-handler` supports `null` header values to remove default
// headers, but its published types only allow strings.
await handler(request, response, config as Parameters<typeof handler>[2]);

// Before returning the response, log the status code and time taken.
const responseTime = Date.now() - requestTime.getTime();
Expand Down
2 changes: 2 additions & 0 deletions tests/__snapshots__/cli.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ exports[`utilities/cli > render help text 1`] = `

-u, --no-compression Do not compress files

--no-cache Disable browser caching

--no-etag Send \`Last-Modified\` header instead of \`ETag\`

-S, --symlinks Resolve symlinks instead of showing 404 errors
Expand Down
42 changes: 42 additions & 0 deletions tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,48 @@ describe('utilities/config', () => {
expect(configuration).toMatchSnapshot();
});

// When `--no-cache` is passed, caching should be disabled for every path.
test('disable cache when requested', async () => {
const configuration = await loadConfig('non-existent', {
'--no-cache': true,
});

expect(configuration).toMatchObject({
etag: false,
headers: [
{
source: '**',
headers: [
{
key: 'Cache-Control',
value: 'no-store, no-cache, must-revalidate, proxy-revalidate',
},
{
key: 'Pragma',
value: 'no-cache',
},
{
key: 'Expires',
value: '0',
},
{
key: 'Surrogate-Control',
value: 'no-store',
},
{
key: 'ETag',
value: null,
},
{
key: 'Last-Modified',
value: null,
},
],
},
],
});
});

// When the configuration source is deprecated, i.e., the configuration lives
// in `now.json` or `package.json`, a warning should be printed.
test('warn when configuration comes from a deprecated source', async () => {
Expand Down
22 changes: 22 additions & 0 deletions tests/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,26 @@ describe('utilities/server', () => {

expect(consoleSpy).not.toHaveBeenCalled();
});

// Make sure cache-related headers can be disabled for served files.
test('disable cache headers', async () => {
const noCacheConfig = await loadConfiguration(process.cwd(), fixture, {
'--no-cache': true,
});
const address = await startServer({ port: 3005 }, noCacheConfig, {
'--no-cache': true,
});

const response = await fetch(address.local!);

expect(response.ok).toBe(true);
expect(response.headers['cache-control']).toBe(
'no-store, no-cache, must-revalidate, proxy-revalidate',
);
expect(response.headers.pragma).toBe('no-cache');
expect(response.headers.expires).toBe('0');
expect(response.headers['surrogate-control']).toBe('no-store');
expect(response.headers.etag).toBeUndefined();
expect(response.headers['last-modified']).toBeUndefined();
});
});