From 761d292221d07da14ec89f871365c4ffefb4d4dc Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 10 Mar 2026 06:20:12 +0000 Subject: [PATCH 1/2] types: remove global static reply augmentation --- README.md | 4 + types/index.d.ts | 50 +++++----- types/index.test-d.ts | 211 ++++++++---------------------------------- 3 files changed, 72 insertions(+), 193 deletions(-) diff --git a/README.md b/README.md index 770cf68..5e34a7e 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ Plugin for serving static files as fast as possible. npm i @fastify/static ``` +TypeScript users get `reply.sendFile()` and `reply.download()` from the plugin +registration itself, so those reply decorators are only available on instances +where `@fastify/static` has actually been registered. + ### Compatibility | Plugin version | Fastify version | diff --git a/types/index.d.ts b/types/index.d.ts index 63874cb..4780a54 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,20 +1,34 @@ -import { FastifyPluginAsync, FastifyReply, FastifyRequest, RouteOptions } from 'fastify' +import { + AnyFastifyInstance, + ApplyDecorators, + FastifyPluginAsync, + FastifyReply, + FastifyRequest, + RouteOptions, + UnEncapsulatedPlugin +} from 'fastify' import { Stats } from 'node:fs' -declare module 'fastify' { - interface FastifyReply { - sendFile(filename: string, rootPath?: string): FastifyReply; - sendFile(filename: string, options?: fastifyStatic.SendOptions): FastifyReply; - sendFile(filename: string, rootPath?: string, options?: fastifyStatic.SendOptions): FastifyReply; - download(filepath: string, options?: fastifyStatic.SendOptions): FastifyReply; - download(filepath: string, filename?: string): FastifyReply; - download(filepath: string, filename?: string, options?: fastifyStatic.SendOptions): FastifyReply; +declare namespace fastifyStatic { + export type FastifyStaticPluginDecorators = { + reply: { + sendFile(filename: string, rootPath?: string): FastifyReply; + sendFile(filename: string, options?: fastifyStatic.SendOptions): FastifyReply; + sendFile(filename: string, rootPath?: string, options?: fastifyStatic.SendOptions): FastifyReply; + download(filepath: string, options?: fastifyStatic.SendOptions): FastifyReply; + download(filepath: string, filename?: string): FastifyReply; + download(filepath: string, filename?: string, options?: fastifyStatic.SendOptions): FastifyReply; + } } -} -type FastifyStaticPlugin = FastifyPluginAsync> + export type FastifyStaticPlugin = UnEncapsulatedPlugin< + FastifyPluginAsync< + NonNullable, + TInstance, + ApplyDecorators + > + > -declare namespace fastifyStatic { export interface SetHeadersResponse { getHeader: FastifyReply['getHeader']; setHeader: FastifyReply['header']; @@ -56,7 +70,6 @@ declare namespace fastifyStatic { export interface ListOptionsJsonFormat extends ListOptions { format: 'json'; - // Required when the URL parameter `format=html` exists render?: ListRender; } @@ -65,7 +78,6 @@ declare namespace fastifyStatic { render: ListRender; } - // Passed on to `send` export interface SendOptions { acceptRanges?: boolean; contentType?: boolean; @@ -94,7 +106,6 @@ declare namespace fastifyStatic { SendOptions & RootOptions & { - // Added by this plugin prefix?: string; prefixAvoidTrailingSlash?: boolean; decorateReply?: boolean; @@ -105,13 +116,7 @@ declare namespace fastifyStatic { globIgnore?: string[]; list?: boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat; allowedPath?: (pathName: string, root: string, request: FastifyRequest) => boolean; - /** - * @description - * Opt-in to looking for pre-compressed files - */ preCompressed?: boolean; - - // Passed on to `send` acceptRanges?: boolean; contentType?: boolean; cacheControl?: boolean; @@ -127,10 +132,9 @@ declare namespace fastifyStatic { } export const fastifyStatic: FastifyStaticPlugin - export { fastifyStatic as default } } -declare function fastifyStatic (...params: Parameters): ReturnType +declare function fastifyStatic (...params: Parameters): ReturnType export = fastifyStatic diff --git a/types/index.test-d.ts b/types/index.test-d.ts index afe4ce4..decb7fb 100644 --- a/types/index.test-d.ts +++ b/types/index.test-d.ts @@ -1,9 +1,10 @@ -import fastify, { FastifyInstance, FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify' -import { Server } from 'node:http' +import fastify, { FastifyReply, FastifyRequest } from 'fastify' import { Stats } from 'node:fs' import { expectAssignable, expectError, expectType } from 'tsd' import * as fastifyStaticStar from '..' import fastifyStatic, { + FastifyStaticPlugin, + FastifyStaticPluginDecorators, FastifyStaticOptions, fastifyStatic as fastifyStaticNamed } from '..' @@ -11,27 +12,23 @@ import fastifyStatic, { const fastifyStaticCjsImport = fastifyStaticStar const fastifyStaticCjs = require('..') -const app: FastifyInstance = fastify() - -app.register(fastifyStatic, { root: __dirname }) -app.register(fastifyStaticNamed, { root: __dirname }) -app.register(fastifyStaticCjs, { root: __dirname }) -app.register(fastifyStaticCjsImport.default, { root: __dirname }) -app.register(fastifyStaticCjsImport.fastifyStatic, { root: __dirname }) -app.register(fastifyStaticStar.default, { root: __dirname }) -app.register(fastifyStaticStar.fastifyStatic, { root: __dirname }) - -expectType>(fastifyStatic) -expectType>(fastifyStaticNamed) -expectType>(fastifyStaticCjsImport.default) -expectType>(fastifyStaticCjsImport.fastifyStatic) -expectType>(fastifyStaticStar.default) -expectType>( - fastifyStaticStar.fastifyStatic -) +const app = fastify() +app.register(fastifyStatic, { root: '/' }) +app.register(fastifyStaticNamed, { root: '/' }) +app.register(fastifyStaticCjs, { root: '/' }) +app.register(fastifyStaticCjsImport.default, { root: '/' }) +app.register(fastifyStaticCjsImport.fastifyStatic, { root: '/' }) +app.register(fastifyStaticStar.default, { root: '/' }) +app.register(fastifyStaticStar.fastifyStatic, { root: '/' }) + +expectType(fastifyStatic) +expectType(fastifyStaticNamed) +expectType(fastifyStaticCjsImport.default) +expectType(fastifyStaticCjsImport.fastifyStatic) +expectType(fastifyStaticStar.default) +expectType(fastifyStaticStar.fastifyStatic) expectType(fastifyStaticCjs) -const appWithImplicitHttp = fastify() const options: FastifyStaticOptions = { acceptRanges: true, contentType: true, @@ -59,13 +56,10 @@ const options: FastifyStaticOptions = { res.setHeader('X-Test', 'string') expectType(path) - expectType(stat) }, preCompressed: false, - allowedPath: (_pathName: string, _root: string, _request: FastifyRequest) => { - return true - }, + allowedPath: (_pathName: string, _root: string, _request: FastifyRequest) => true, constraints: { host: /^.*\.example\.com$/, version: '1.0.2' @@ -80,161 +74,38 @@ expectError({ expectAssignable({ root: '', - list: { - format: 'json' - } -}) - -expectAssignable({ - root: '', - list: { - format: 'json', - render: () => '' - } + list: { format: 'json' } }) expectAssignable({ root: '', - list: { - format: 'html', - render: () => '' - } + list: { format: 'html', render: () => '' } }) expectError({ root: '', - list: { - format: 'html' - } -}) - -expectAssignable({ - root: [''] + list: { format: 'html' } }) -expectAssignable({ - root: new URL('') +expectAssignable({ root: [''] }) +expectAssignable({ root: new URL('file:///tmp') }) +expectAssignable({ root: [new URL('file:///tmp')] }) +expectError({ serve: true }) +expectAssignable({ serve: true, root: '' }) +expectAssignable({ serve: false }) + +const registered = fastify().register(fastifyStatic, options) +registered.get('/', (_request, reply) => { + expectType(reply.sendFile) + expectType(reply.download) + reply.sendFile('some-file-name') + reply.sendFile('some-file-name', { cacheControl: false }) + reply.download('some-file-name') + reply.download('some-file-name', 'custom-name', { contentType: false }) }) -expectAssignable({ - root: [new URL('')] -}) - -expectError({ - serve: true -}) - -expectAssignable({ - serve: true, - root: '' -}) - -expectAssignable({ - serve: false +const serverWithHttp2 = fastify({ http2: true }).register(fastifyStatic, { root: '/' }) +serverWithHttp2.get('/', (_request, reply) => { + reply.sendFile('some-file-name') + reply.download('some-file-name') }) - -appWithImplicitHttp - .register(fastifyStatic, options) - .after(() => { - appWithImplicitHttp.get('/', (_request, reply) => { - reply.sendFile('some-file-name') - }) - }) - -const appWithHttp2 = fastify({ http2: true }) - -appWithHttp2 - .register(fastifyStatic, options) - .after(() => { - appWithHttp2.get('/', (_request, reply) => { - reply.sendFile('some-file-name') - }) - - appWithHttp2.get('/download', (_request, reply) => { - reply.download('some-file-name') - }) - - appWithHttp2.get('/download/1', (_request, reply) => { - reply.download('some-file-name', { maxAge: '2 days' }) - }) - - appWithHttp2.get('/download/2', (_request, reply) => { - reply.download('some-file-name', 'some-filename', { cacheControl: false, acceptRanges: true }) - }) - - appWithHttp2.get('/download/3', (_request, reply) => { - reply.download('some-file-name', 'some-filename', { contentType: false }) - }) - }) - -const multiRootAppWithImplicitHttp = fastify() -options.root = [''] - -multiRootAppWithImplicitHttp - .register(fastifyStatic, options) - .after(() => { - multiRootAppWithImplicitHttp.get('/', (_request, reply) => { - reply.sendFile('some-file-name') - }) - - multiRootAppWithImplicitHttp.get('/', (_request, reply) => { - reply.sendFile('some-file-name', { cacheControl: false, acceptRanges: true }) - }) - - multiRootAppWithImplicitHttp.get('/', (_request, reply) => { - reply.sendFile('some-file-name', 'some-root-name', { cacheControl: false, acceptRanges: true }) - }) - - multiRootAppWithImplicitHttp.get('/', (_request, reply) => { - reply.sendFile('some-file-name', 'some-root-name-2', { contentType: false }) - }) - - multiRootAppWithImplicitHttp.get('/download', (_request, reply) => { - reply.download('some-file-name') - }) - - multiRootAppWithImplicitHttp.get('/download/1', (_request, reply) => { - reply.download('some-file-name', { maxAge: '2 days' }) - }) - - multiRootAppWithImplicitHttp.get('/download/2', (_request, reply) => { - reply.download('some-file-name', 'some-filename', { cacheControl: false, acceptRanges: true }) - }) - - multiRootAppWithImplicitHttp.get('/download/3', (_request, reply) => { - reply.download('some-file-name', 'some-filename', { contentType: false }) - }) - }) - -const noIndexApp = fastify() -options.root = '' -options.index = false - -noIndexApp - .register(fastifyStatic, options) - .after(() => { - noIndexApp.get('/', (_request, reply) => { - reply.send('

fastify-static

') - }) - }) - -options.root = new URL('') - -const URLRootApp = fastify() -URLRootApp.register(fastifyStatic, options) - .after(() => { - URLRootApp.get('/', (_request, reply) => { - reply.send('

fastify-static

') - }) - }) - -const defaultIndexApp = fastify() -options.index = 'index.html' - -defaultIndexApp - .register(fastifyStatic, options) - .after(() => { - defaultIndexApp.get('/', (_request, reply) => { - reply.send('

fastify-static

') - }) - }) From ed8f5386ed76395f4cc710a622e910a4333aecca Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sun, 15 Mar 2026 17:41:07 +0000 Subject: [PATCH 2/2] ci: pin fastify dev dependency to typed-decorators branch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 749fd3c..0a16c56 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "c8": "^10.1.3", "concat-stream": "^2.0.0", "eslint": "^9.17.0", - "fastify": "^5.1.0", + "fastify": "github:fastify/fastify#feat/typed-decorators", "neostandard": "^0.12.0", "pino": "^10.0.0", "proxyquire": "^2.1.3",