From cfd9d26d86ac0528db7aae159405528823b28697 Mon Sep 17 00:00:00 2001 From: LitoMore Date: Sun, 3 May 2026 20:43:09 +0800 Subject: [PATCH] Add `--no-cache` flag --- source/types.ts | 3 +- source/utilities/cli.ts | 3 ++ source/utilities/config.ts | 39 ++++++++++++++++++++++++-- source/utilities/server.ts | 4 ++- tests/__snapshots__/cli.test.ts.snap | 2 ++ tests/config.test.ts | 42 ++++++++++++++++++++++++++++ tests/server.test.ts | 22 +++++++++++++++ 7 files changed, 111 insertions(+), 4 deletions(-) diff --git a/source/types.ts b/source/types.ts index 9294d7c3..a16148fd 100644 --- a/source/types.ts +++ b/source/types.ts @@ -48,7 +48,7 @@ export declare interface Header { source: string; headers: { key: string; - value: string; + value: string | null; }[]; } @@ -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; diff --git a/source/utilities/cli.ts b/source/utilities/cli.ts index 726c5268..a43a0100 100644 --- a/source/utilities/cli.ts +++ b/source/utilities/cli.ts @@ -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 @@ -152,6 +154,7 @@ const options = { '--config': String, '--no-clipboard': Boolean, '--no-compression': Boolean, + '--no-cache': Boolean, '--no-etag': Boolean, '--symlinks': Boolean, '--cors': Boolean, diff --git a/source/utilities/config.ts b/source/utilities/config.ts index db2e3c85..67b2836d 100644 --- a/source/utilities/config.ts +++ b/source/utilities/config.ts @@ -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. @@ -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; }; diff --git a/source/utilities/server.ts b/source/utilities/server.ts index d7a22077..ba0f9672 100644 --- a/source/utilities/server.ts +++ b/source/utilities/server.ts @@ -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[2]); // Before returning the response, log the status code and time taken. const responseTime = Date.now() - requestTime.getTime(); diff --git a/tests/__snapshots__/cli.test.ts.snap b/tests/__snapshots__/cli.test.ts.snap index 9cddf0e0..d1282b15 100644 --- a/tests/__snapshots__/cli.test.ts.snap +++ b/tests/__snapshots__/cli.test.ts.snap @@ -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 diff --git a/tests/config.test.ts b/tests/config.test.ts index 3240d1f0..afdd7282 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -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 () => { diff --git a/tests/server.test.ts b/tests/server.test.ts index 3656bd31..1ef12395 100644 --- a/tests/server.test.ts +++ b/tests/server.test.ts @@ -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(); + }); });