From 1fc30c1cab499a4315db96de7dae7b22f27aa8c3 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 10 Mar 2026 06:18:59 +0000 Subject: [PATCH 1/3] feat: add createPlugin typing helper --- README.md | 35 ++++++++ plugin.js | 11 +++ types/plugin.d.ts | 65 ++++++++++---- types/plugin.test-d.ts | 188 ++++++++++++++++------------------------- 4 files changed, 169 insertions(+), 130 deletions(-) diff --git a/README.md b/README.md index a91536c..5638525 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,41 @@ module.exports = fp(async function (fastify, opts) { }) ``` +### `createPlugin()` + +For TypeScript users that want Fastify to infer decorators from plugin return +values, `fastify-plugin` also exposes `createPlugin()`. + +```ts +import { createPlugin } from 'fastify-plugin' + +export default createPlugin(async (fastify) => { + return fastify.decorate('usersRepository', { + findAll () { + return [] + } + }) +}) +``` + +`createPlugin()` also accepts type-level plugin dependencies so a plugin can +express that it expects decorators from previously registered plugins: + +```ts +import { createPlugin } from 'fastify-plugin' +import dbPlugin from './db-plugin' + +export default createPlugin((fastify) => { + fastify.usersRepository.findAll() + return fastify +}, { + dependencies: [dbPlugin] +}) +``` + +Use `fp()` for legacy plugins and runtime metadata checks. Use `createPlugin()` +when you want inference-friendly plugin composition. + ## Metadata In addition, if you use this module when creating new plugins, you can declare the dependencies, the name, and the expected Fastify version that your plugin needs. diff --git a/plugin.js b/plugin.js index 3ad686e..701d65a 100644 --- a/plugin.js +++ b/plugin.js @@ -62,6 +62,17 @@ function plugin (fn, options = {}) { return fn } +function createPlugin (fn, options = {}) { + const runtimeOptions = { ...options } + + if (Array.isArray(runtimeOptions.dependencies)) { + delete runtimeOptions.dependencies + } + + return plugin(fn, runtimeOptions) +} + module.exports = plugin module.exports.default = plugin module.exports.fastifyPlugin = plugin +module.exports.createPlugin = createPlugin diff --git a/types/plugin.d.ts b/types/plugin.d.ts index 7f66553..7c08ff5 100644 --- a/types/plugin.d.ts +++ b/types/plugin.d.ts @@ -1,12 +1,9 @@ import { - FastifyPluginCallback, + ApplyDependencies, + FastifyDependencies, FastifyPluginAsync, - FastifyPluginOptions, - RawServerBase, - RawServerDefault, - FastifyTypeProvider, - FastifyTypeProviderDefault, - FastifyBaseLogger, + FastifyPluginCallback, + UnEncapsulatedPlugin } from 'fastify' type FastifyPlugin = typeof fastifyPlugin @@ -26,6 +23,15 @@ declare namespace fastifyPlugin { dependencies?: string[], encapsulate?: boolean } + + /** + * Metadata accepted by `createPlugin()`. + * `dependencies` are type-level plugin dependencies rather than runtime plugin names. + */ + export interface CreatePluginMetadata extends Omit { + dependencies?: TDependencies + } + // Exporting PluginOptions for backward compatibility after renaming it to PluginMetadata /** * @deprecated Use PluginMetadata instead @@ -34,6 +40,34 @@ declare namespace fastifyPlugin { export const fastifyPlugin: FastifyPlugin export { fastifyPlugin as default } + + export function createPlugin any> ( + plugin: TPlugin extends FastifyPluginCallback ? TPlugin : never, + options?: Omit + ): UnEncapsulatedPlugin + + export function createPlugin < + TPlugin extends (...args: any[]) => any, + TDependencies extends FastifyDependencies, + TEnhanced extends ApplyDependencies>, TDependencies> = ApplyDependencies>, TDependencies> + > ( + plugin: TEnhanced, + options: CreatePluginMetadata + ): UnEncapsulatedPlugin + + export function createPlugin any> ( + plugin: TPlugin extends FastifyPluginAsync ? TPlugin : never, + options?: Omit + ): UnEncapsulatedPlugin + + export function createPlugin < + TPlugin extends (...args: any[]) => any, + TDependencies extends FastifyDependencies, + TEnhanced extends ApplyDependencies>, TDependencies> = ApplyDependencies>, TDependencies> + > ( + plugin: TEnhanced, + options: CreatePluginMetadata + ): UnEncapsulatedPlugin } /** @@ -45,15 +79,14 @@ declare namespace fastifyPlugin { * @param options Optional plugin options */ -declare function fastifyPlugin< - Options extends FastifyPluginOptions = Record, - RawServer extends RawServerBase = RawServerDefault, - TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault, - Logger extends FastifyBaseLogger = FastifyBaseLogger, - Fn extends FastifyPluginCallback | FastifyPluginAsync = FastifyPluginCallback -> ( - fn: Fn extends unknown ? Fn extends (...args: any) => Promise ? FastifyPluginAsync : FastifyPluginCallback : Fn, +declare function fastifyPlugin any> ( + fn: TPlugin extends FastifyPluginCallback ? TPlugin : never, + options?: fastifyPlugin.PluginMetadata | string +): UnEncapsulatedPlugin> + +declare function fastifyPlugin any> ( + fn: TPlugin extends FastifyPluginAsync ? TPlugin : never, options?: fastifyPlugin.PluginMetadata | string -): Fn +): UnEncapsulatedPlugin> export = fastifyPlugin diff --git a/types/plugin.test-d.ts b/types/plugin.test-d.ts index 21b5ffa..b5562be 100644 --- a/types/plugin.test-d.ts +++ b/types/plugin.test-d.ts @@ -1,71 +1,47 @@ -import fastifyPlugin from '..' -import fastify, { FastifyPluginCallback, FastifyPluginAsync, FastifyError, FastifyInstance, FastifyPluginOptions, RawServerDefault, FastifyTypeProviderDefault, FastifyBaseLogger } from 'fastify' +import fastifyPlugin, { createPlugin } from '..' +import fastify, { + AnyFastifyInstance, + FastifyError, + FastifyInstance, + FastifyPluginAsync, + FastifyPluginCallback, + FastifyPluginOptions, + UnEncapsulatedPlugin +} from 'fastify' import { expectAssignable, expectError, expectNotType, expectType } from 'tsd' import { Server } from 'node:https' import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox' import fastifyExampleCallback from './example-callback.test-d' import fastifyExampleAsync from './example-async.test-d' -interface Options { +interface Options extends FastifyPluginOptions { foo: string } const testSymbol = Symbol('foobar') -// Callback - -const pluginCallback: FastifyPluginCallback = (_fastify, _options, _next) => { } -expectType(fastifyPlugin(pluginCallback)) - -const pluginCallbackWithTypes = (_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { } -expectAssignable(fastifyPlugin(pluginCallbackWithTypes)) -expectNotType(fastifyPlugin(pluginCallbackWithTypes)) - -expectAssignable(fastifyPlugin((_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { })) -expectNotType(fastifyPlugin((_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { })) - -expectType(fastifyPlugin(pluginCallback, '')) -expectType(fastifyPlugin(pluginCallback, { - fastify: '', - name: '', - decorators: { - fastify: ['', testSymbol], - reply: ['', testSymbol], - request: ['', testSymbol] - }, - dependencies: [''], - encapsulate: true -})) - -const pluginCallbackWithOptions: FastifyPluginCallback = (_fastify, options, _next) => { +const legacyCallback: FastifyPluginCallback = (_fastify, options, done) => { expectType(options.foo) + done() } +expectAssignable>>( + fastifyPlugin(legacyCallback) +) +expectNotType(fastifyPlugin(legacyCallback)) -expectType>(fastifyPlugin(pluginCallbackWithOptions)) - -const pluginCallbackWithServer: FastifyPluginCallback = (fastify, _options, _next) => { - expectType(fastify.server) +const legacyAsync: FastifyPluginAsync = async (_fastify, options) => { + expectType(options.foo) } +expectAssignable>>( + fastifyPlugin(legacyAsync) +) +expectNotType(fastifyPlugin(legacyAsync)) -expectType>(fastifyPlugin(pluginCallbackWithServer)) - -const pluginCallbackWithTypeProvider: FastifyPluginCallback = (_fastify, _options, _next) => { } - -expectType>(fastifyPlugin(pluginCallbackWithTypeProvider)) - -// Async - -const pluginAsync: FastifyPluginAsync = async (_fastify, _options) => { } -expectType(fastifyPlugin(pluginAsync)) - -const pluginAsyncWithTypes = async (_fastify: FastifyInstance, _options: FastifyPluginOptions): Promise => { } -expectType>(fastifyPlugin(pluginAsyncWithTypes)) - -expectType>(fastifyPlugin(async (_fastify: FastifyInstance, _options: FastifyPluginOptions): Promise => { })) -expectType(fastifyPlugin(pluginAsync, '')) -expectType(fastifyPlugin(pluginAsync, { +const wrappedCallback = fastifyPlugin((fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void) => { + return fastify.decorate('wrappedCallback', true) +}, { fastify: '', - name: '', + name: 'wrapped-callback', decorators: { fastify: ['', testSymbol], reply: ['', testSymbol], @@ -73,94 +49,78 @@ expectType(fastifyPlugin(pluginAsync, { }, dependencies: [''], encapsulate: true -})) - -const pluginAsyncWithOptions: FastifyPluginAsync = async (_fastify, options) => { - expectType(options.foo) -} +}) +expectAssignable>(wrappedCallback) -expectType>(fastifyPlugin(pluginAsyncWithOptions)) +const wrappedAsync = fastifyPlugin(async (fastify: FastifyInstance) => { + return fastify.decorate('wrappedAsync', true) +}) +expectAssignable>(wrappedAsync) -const pluginAsyncWithServer: FastifyPluginAsync = async (fastify, _options) => { +const callbackWithServer = fastifyPlugin((fastify: FastifyInstance) => { expectType(fastify.server) -} - -expectType>(fastifyPlugin(pluginAsyncWithServer)) - -const pluginAsyncWithTypeProvider: FastifyPluginAsync = async (_fastify, _options) => { } - -expectType>(fastifyPlugin(pluginAsyncWithTypeProvider)) - -// Fastify register - -const server = fastify() -server.register(fastifyPlugin(pluginCallback)) -server.register(fastifyPlugin(pluginCallbackWithTypes), { foo: 'bar' }) -server.register(fastifyPlugin(pluginCallbackWithOptions), { foo: 'bar' }) -server.register(fastifyPlugin(pluginCallbackWithServer), { foo: 'bar' }) -server.register(fastifyPlugin(pluginCallbackWithTypeProvider), { foo: 'bar' }) -server.register(fastifyPlugin(pluginAsync)) -server.register(fastifyPlugin(pluginAsyncWithTypes), { foo: 'bar' }) -server.register(fastifyPlugin(pluginAsyncWithOptions), { foo: 'bar' }) -server.register(fastifyPlugin(pluginAsyncWithServer), { foo: 'bar' }) -server.register(fastifyPlugin(pluginAsyncWithTypeProvider), { foo: 'bar' }) - -// properly handling callback and async -fastifyPlugin(function (fastify, options, next) { - expectType(fastify) - expectType>(options) - expectType<(err?: Error) => void>(next) + return fastify }) +expectAssignable< + UnEncapsulatedPlugin, FastifyInstance>> +>(callbackWithServer) -fastifyPlugin(function (fastify, options, next) { - expectType(fastify) - expectType(options) - expectType<(err?: Error) => void>(next) +const asyncWithTypeProvider = fastifyPlugin(async (fastify: FastifyInstance, options: Options) => { + expectType(options.foo) + return fastify.withTypeProvider() }) +expectAssignable>>(asyncWithTypeProvider) -fastifyPlugin(async function (fastify, options) { - expectType(fastify) - expectType(options) +const createdPlugin = createPlugin((instance: FastifyInstance) => { + return instance.decorate('createdPlugin', 42) }) +expectNotType(createdPlugin) -expectAssignable>(fastifyPlugin(async function (_fastify: FastifyInstance, _options: Options) { })) -expectNotType(fastifyPlugin(async function (_fastify: FastifyInstance, _options: Options) { })) +const app = fastify() +app.register(fastifyPlugin(legacyCallback), { foo: 'bar' }) +app.register(fastifyPlugin(legacyAsync), { foo: 'bar' }) +app.register(wrappedCallback) +app.register(wrappedAsync) +app.register(createdPlugin) -fastifyPlugin(async function (fastify, options: Options) { - expectType(fastify) +fastifyPlugin(function (fastify, options: Options, next: (err?: Error) => void) { + expectAssignable(fastify) expectType(options) + expectType<(err?: Error) => void>(next) + return fastify }) -fastifyPlugin(async function (fastify, options) { - expectType(fastify) - expectType>(options) +fastifyPlugin(async function (fastify, options: Options) { + expectAssignable(fastify) + expectType(options) + return fastify }) expectError( fastifyPlugin(async function (fastify, options: Options, _next) { - expectType(fastify) + expectAssignable(fastify) expectType(options) + return fastify }) ) -expectAssignable>(fastifyPlugin(function (_fastify, _options, _next) { })) -expectNotType(fastifyPlugin(function (_fastify, _options, _next) { })) - -fastifyPlugin(function (fastify, options: Options, next) { - expectType(fastify) - expectType(options) - expectType<(err?: Error) => void>(next) -}) expectError( fastifyPlugin(function (fastify, options: Options, _next) { - expectType(fastify) + expectAssignable(fastify) expectType(options) - return Promise.resolve() + return Promise.resolve(fastify) }) ) -server.register(fastifyExampleCallback, { foo: 'bar' }) -expectError(server.register(fastifyExampleCallback, { foo: 'baz' })) +const pluginCallbackWithTypes = (fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): FastifyInstance => fastify +expectAssignable>(fastifyPlugin(pluginCallbackWithTypes)) + +const pluginAsyncWithTypes = async (fastify: FastifyInstance, _options: FastifyPluginOptions): Promise => fastify +expectAssignable>(fastifyPlugin(pluginAsyncWithTypes)) + +const exampleApp = fastify() +exampleApp.register(fastifyExampleCallback, { foo: 'bar' } as const) +expectError(exampleApp.register(fastifyExampleCallback, { foo: 'baz' } as const)) -server.register(fastifyExampleAsync, { foo: 'bar' }) -expectError(server.register(fastifyExampleAsync, { foo: 'baz' })) +exampleApp.register(fastifyExampleAsync, { foo: 'bar' } as const) +expectError(exampleApp.register(fastifyExampleAsync, { foo: 'baz' } as const)) From 661ccebacc02f99e85d5250935c2c81aa41c0e81 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sun, 15 Mar 2026 10:16:14 +0000 Subject: [PATCH 2/3] test: cover createPlugin runtime dependency handling --- test/test.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/test.js b/test/test.js index 522083f..6419e65 100644 --- a/test/test.js +++ b/test/test.js @@ -12,6 +12,50 @@ test('fastify-plugin is a function', (t) => { t.assert.ok(typeof fp === 'function') }) +test('createPlugin is exported', (t) => { + t.plan(1) + t.assert.ok(typeof fp.createPlugin === 'function') +}) + +test('createPlugin removes runtime dependencies array metadata', (t) => { + t.plan(1) + + function plugin (_fastify, _opts, next) { + next() + } + + const wrapped = fp.createPlugin(plugin, { + dependencies: ['a', 'b'], + fastify: '5.x', + name: 'runtime-deps-test' + }) + + t.assert.deepStrictEqual(wrapped[Symbol.for('plugin-meta')], { + fastify: '5.x', + name: 'runtime-deps-test' + }) +}) + +test('createPlugin keeps non-array dependencies metadata untouched', (t) => { + t.plan(1) + + function plugin (_fastify, _opts, next) { + next() + } + + const wrapped = fp.createPlugin(plugin, { + dependencies: 'legacy-dependency', + fastify: '5.x', + name: 'runtime-deps-string-test' + }) + + t.assert.deepStrictEqual(wrapped[Symbol.for('plugin-meta')], { + dependencies: 'legacy-dependency', + fastify: '5.x', + name: 'runtime-deps-string-test' + }) +}) + test('should return the function with the skip-override Symbol', (t) => { t.plan(1) From 9b181ad5c352c5f41ef970044138175ceb07526e Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sun, 15 Mar 2026 17:41:00 +0000 Subject: [PATCH 3/3] 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 a2b84d9..60009a4 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@types/node": "^25.0.3", "c8": "^11.0.0", "eslint": "^9.17.0", - "fastify": "^5.0.0", + "fastify": "github:fastify/fastify#feat/typed-decorators", "neostandard": "^0.12.0", "proxyquire": "^2.1.3", "tsd": "^0.33.0"