├── .gitattributes ├── .github ├── dependabot.yml ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── eslint.config.js ├── lib ├── getPluginName.js └── toCamelCase.js ├── package.json ├── plugin.js ├── test ├── bundlers.test.js ├── checkVersion.test.js ├── composite.test.js ├── esm │ ├── esm.mjs │ └── index.test.js ├── extractPluginName.test.js ├── mu1tip1e.composite.test.js ├── test.js └── toCamelCase.test.js └── types ├── example-async.test-d.ts ├── example-callback.test-d.ts ├── plugin.d.ts └── plugin.test-d.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically convert line endings 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 15 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - "discussion" 8 | - "feature request" 9 | - "bug" 10 | - "help wanted" 11 | - "plugin suggestion" 12 | - "good first issue" 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | - 'v*' 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | pull_request: 13 | paths-ignore: 14 | - 'docs/**' 15 | - '*.md' 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | test: 22 | permissions: 23 | contents: write 24 | pull-requests: write 25 | uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5 26 | with: 27 | license-check: true 28 | lint: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Vim swap files 133 | *.swp 134 | 135 | # macOS files 136 | .DS_Store 137 | 138 | # Clinic 139 | .clinic 140 | 141 | # lock files 142 | bun.lockb 143 | package-lock.json 144 | pnpm-lock.yaml 145 | yarn.lock 146 | 147 | # editor files 148 | .vscode 149 | .idea 150 | 151 | #tap files 152 | .tap/ 153 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Fastify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastify-plugin 2 | 3 | [![CI](https://github.com/fastify/fastify-plugin/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-plugin/actions/workflows/ci.yml) 4 | [![NPM version](https://img.shields.io/npm/v/fastify-plugin.svg?style=flat)](https://www.npmjs.com/package/fastify-plugin) 5 | [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) 6 | 7 | `fastify-plugin` is a plugin helper for [Fastify](https://github.com/fastify/fastify). 8 | 9 | When you build plugins for Fastify and you want them to be accessible in the same context where you require them, you have two ways: 10 | 1. Use the `skip-override` hidden property 11 | 2. Use this module 12 | 13 | __Note: the v4.x series of this module covers Fastify v4__ 14 | __Note: the v2.x & v3.x series of this module covers Fastify v3. For Fastify v2 support, refer to the v1.x series.__ 15 | 16 | ## Install 17 | 18 | ```sh 19 | npm i fastify-plugin 20 | ``` 21 | 22 | ## Usage 23 | `fastify-plugin` can do three things for you: 24 | - Add the `skip-override` hidden property 25 | - Check the bare-minimum version of Fastify 26 | - Pass some custom metadata of the plugin to Fastify 27 | 28 | Example using a callback: 29 | ```js 30 | const fp = require('fastify-plugin') 31 | 32 | module.exports = fp(function (fastify, opts, done) { 33 | // your plugin code 34 | done() 35 | }) 36 | ``` 37 | 38 | Example using an [async](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) function: 39 | ```js 40 | const fp = require('fastify-plugin') 41 | 42 | // A callback function param is not required for async functions 43 | module.exports = fp(async function (fastify, opts) { 44 | // Wait for an async function to fulfill promise before proceeding 45 | await exampleAsyncFunction() 46 | }) 47 | ``` 48 | 49 | ## Metadata 50 | 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. 51 | 52 | #### Fastify version 53 | If you need to set a bare-minimum version of Fastify for your plugin, just add the [semver](https://semver.org/) range that you need: 54 | ```js 55 | const fp = require('fastify-plugin') 56 | 57 | module.exports = fp(function (fastify, opts, done) { 58 | // your plugin code 59 | done() 60 | }, { fastify: '5.x' }) 61 | ``` 62 | 63 | If you need to check the Fastify version only, you can pass just the version string. 64 | 65 | You can check [here](https://github.com/npm/node-semver#ranges) how to define a `semver` range. 66 | 67 | #### Name 68 | Fastify uses this option to validate the dependency graph, allowing it to ensure that no name collisions occur and making it possible to perform [dependency checks](https://github.com/fastify/fastify-plugin#dependencies). 69 | 70 | ```js 71 | const fp = require('fastify-plugin') 72 | 73 | function plugin (fastify, opts, done) { 74 | // your plugin code 75 | done() 76 | } 77 | 78 | module.exports = fp(plugin, { 79 | fastify: '5.x', 80 | name: 'your-plugin-name' 81 | }) 82 | ``` 83 | 84 | #### Dependencies 85 | You can also check if the `plugins` and `decorators` that your plugin intend to use are present in the dependency graph. 86 | > *Note:* This is the point where registering `name` of the plugins become important, because you can reference `plugin` dependencies by their [name](https://github.com/fastify/fastify-plugin#name). 87 | ```js 88 | const fp = require('fastify-plugin') 89 | 90 | function plugin (fastify, opts, done) { 91 | // your plugin code 92 | done() 93 | } 94 | 95 | module.exports = fp(plugin, { 96 | fastify: '5.x', 97 | decorators: { 98 | fastify: ['plugin1', 'plugin2'], 99 | reply: ['compress'] 100 | }, 101 | dependencies: ['plugin1-name', 'plugin2-name'] 102 | }) 103 | ``` 104 | 105 | #### Encapsulate 106 | 107 | By default, `fastify-plugin` breaks the [encapsulation](https://github.com/fastify/fastify/blob/HEAD/docs/Reference/Encapsulation.md) but you can optionally keep the plugin encapsulated. 108 | This allows you to set the plugin's name and validate its dependencies without making the plugin accessible. 109 | ```js 110 | const fp = require('fastify-plugin') 111 | 112 | function plugin (fastify, opts, done) { 113 | // the decorator is not accessible outside this plugin 114 | fastify.decorate('util', function() {}) 115 | done() 116 | } 117 | 118 | module.exports = fp(plugin, { 119 | name: 'my-encapsulated-plugin', 120 | fastify: '5.x', 121 | decorators: { 122 | fastify: ['plugin1', 'plugin2'], 123 | reply: ['compress'] 124 | }, 125 | dependencies: ['plugin1-name', 'plugin2-name'], 126 | encapsulate: true 127 | }) 128 | ``` 129 | 130 | #### Bundlers and Typescript 131 | `fastify-plugin` adds a `.default` and `[name]` property to the passed in function. 132 | The type definition would have to be updated to leverage this. 133 | 134 | ## Known Issue: TypeScript Contextual Inference 135 | 136 | [Documentation Reference](https://www.typescriptlang.org/docs/handbook/functions.html#inferring-the-types) 137 | 138 | It is common for developers to inline their plugin with fastify-plugin such as: 139 | 140 | ```js 141 | fp((fastify, opts, done) => { done() }) 142 | fp(async (fastify, opts) => { return }) 143 | ``` 144 | 145 | TypeScript can sometimes infer the types of the arguments for these functions. Plugins in Fastify are recommended to be typed using either `FastifyPluginCallback` or `FastifyPluginAsync`. These two definitions only differ in two ways: 146 | 147 | 1. The third argument `done` (the callback part) 148 | 2. The return type `FastifyPluginCallback` or `FastifyPluginAsync` 149 | 150 | At this time, TypeScript inference is not smart enough to differentiate by definition argument length alone. 151 | 152 | Thus, if you are a TypeScript developer please use on the following patterns instead: 153 | 154 | ```ts 155 | // Callback 156 | 157 | // Assign type directly 158 | const pluginCallback: FastifyPluginCallback = (fastify, options, done) => { } 159 | fp(pluginCallback) 160 | 161 | // or define your own function declaration that satisfies the existing definitions 162 | const pluginCallbackWithTypes = (fastify: FastifyInstance, options: FastifyPluginOptions, done: (error?: FastifyError) => void): void => { } 163 | fp(pluginCallbackWithTypes) 164 | // or inline 165 | fp((fastify: FastifyInstance, options: FastifyPluginOptions, done: (error?: FastifyError) => void): void => { }) 166 | 167 | // Async 168 | 169 | // Assign type directly 170 | const pluginAsync: FastifyPluginAsync = async (fastify, options) => { } 171 | fp(pluginAsync) 172 | 173 | // or define your own function declaration that satisfies the existing definitions 174 | const pluginAsyncWithTypes = async (fastify: FastifyInstance, options: FastifyPluginOptions): Promise => { } 175 | fp(pluginAsyncWithTypes) 176 | // or inline 177 | fp(async (fastify: FastifyInstance, options: FastifyPluginOptions): Promise => { }) 178 | ``` 179 | 180 | ## Acknowledgments 181 | 182 | This project is kindly sponsored by: 183 | - [nearForm](https://nearform.com) 184 | - [LetzDoIt](https://www.letzdoitapp.com/) 185 | 186 | ## License 187 | 188 | Licensed under [MIT](./LICENSE). 189 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ignores: require('neostandard').resolveIgnoresFromGitignore(), 5 | ts: true 6 | }) 7 | -------------------------------------------------------------------------------- /lib/getPluginName.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fpStackTracePattern = /at\s(?:.*\.)?plugin\s.*\n\s*(.*)/ 4 | const fileNamePattern = /(\w*(\.\w*)*)\..*/ 5 | 6 | module.exports = function getPluginName (fn) { 7 | if (fn.name.length > 0) return fn.name 8 | 9 | const stackTraceLimit = Error.stackTraceLimit 10 | Error.stackTraceLimit = 10 11 | try { 12 | throw new Error('anonymous function') 13 | } catch (e) { 14 | Error.stackTraceLimit = stackTraceLimit 15 | return extractPluginName(e.stack) 16 | } 17 | } 18 | 19 | function extractPluginName (stack) { 20 | const m = stack.match(fpStackTracePattern) 21 | 22 | // get last section of path and match for filename 23 | return m ? m[1].split(/[/\\]/).slice(-1)[0].match(fileNamePattern)[1] : 'anonymous' 24 | } 25 | module.exports.extractPluginName = extractPluginName 26 | -------------------------------------------------------------------------------- /lib/toCamelCase.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function toCamelCase (name) { 4 | if (name[0] === '@') { 5 | name = name.slice(1).replace('/', '-') 6 | } 7 | return name.replace(/-(.)/g, function (match, g1) { 8 | return g1.toUpperCase() 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-plugin", 3 | "version": "5.0.1", 4 | "description": "Plugin helper for Fastify", 5 | "main": "plugin.js", 6 | "type": "commonjs", 7 | "types": "types/plugin.d.ts", 8 | "scripts": { 9 | "lint": "eslint", 10 | "lint:fix": "eslint --fix", 11 | "test": "npm run test:unit && npm run test:typescript", 12 | "test:unit": "c8 --100 node --test", 13 | "test:coverage": "c8 node --test && c8 report --reporter=html", 14 | "test:typescript": "tsd" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/fastify/fastify-plugin.git" 19 | }, 20 | "keywords": [ 21 | "plugin", 22 | "helper", 23 | "fastify" 24 | ], 25 | "author": "Tomas Della Vedova - @delvedor (http://delved.org)", 26 | "contributors": [ 27 | { 28 | "name": "Matteo Collina", 29 | "email": "hello@matteocollina.com" 30 | }, 31 | { 32 | "name": "Manuel Spigolon", 33 | "email": "behemoth89@gmail.com" 34 | }, 35 | { 36 | "name": "Aras Abbasi", 37 | "email": "aras.abbasi@gmail.com" 38 | }, 39 | { 40 | "name": "Frazer Smith", 41 | "email": "frazer.dev@icloud.com", 42 | "url": "https://github.com/fdawgs" 43 | } 44 | ], 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/fastify/fastify-plugin/issues" 48 | }, 49 | "homepage": "https://github.com/fastify/fastify-plugin#readme", 50 | "funding": [ 51 | { 52 | "type": "github", 53 | "url": "https://github.com/sponsors/fastify" 54 | }, 55 | { 56 | "type": "opencollective", 57 | "url": "https://opencollective.com/fastify" 58 | } 59 | ], 60 | "devDependencies": { 61 | "@fastify/pre-commit": "^2.1.0", 62 | "@fastify/type-provider-typebox": "^5.0.0-pre.fv5.1", 63 | "@types/node": "^22.0.0", 64 | "c8": "^10.1.2", 65 | "eslint": "^9.17.0", 66 | "fastify": "^5.0.0", 67 | "neostandard": "^0.12.0", 68 | "proxyquire": "^2.1.3", 69 | "tsd": "^0.32.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const getPluginName = require('./lib/getPluginName') 4 | const toCamelCase = require('./lib/toCamelCase') 5 | 6 | let count = 0 7 | 8 | function plugin (fn, options = {}) { 9 | let autoName = false 10 | 11 | if (fn.default !== undefined) { 12 | // Support for 'export default' behaviour in transpiled ECMAScript module 13 | fn = fn.default 14 | } 15 | 16 | if (typeof fn !== 'function') { 17 | throw new TypeError( 18 | `fastify-plugin expects a function, instead got a '${typeof fn}'` 19 | ) 20 | } 21 | 22 | if (typeof options === 'string') { 23 | options = { 24 | fastify: options 25 | } 26 | } 27 | 28 | if ( 29 | typeof options !== 'object' || 30 | Array.isArray(options) || 31 | options === null 32 | ) { 33 | throw new TypeError('The options object should be an object') 34 | } 35 | 36 | if (options.fastify !== undefined && typeof options.fastify !== 'string') { 37 | throw new TypeError(`fastify-plugin expects a version string, instead got '${typeof options.fastify}'`) 38 | } 39 | 40 | if (!options.name) { 41 | autoName = true 42 | options.name = getPluginName(fn) + '-auto-' + count++ 43 | } 44 | 45 | fn[Symbol.for('skip-override')] = options.encapsulate !== true 46 | fn[Symbol.for('fastify.display-name')] = options.name 47 | fn[Symbol.for('plugin-meta')] = options 48 | 49 | // Faux modules support 50 | if (!fn.default) { 51 | fn.default = fn 52 | } 53 | 54 | // TypeScript support for named imports 55 | // See https://github.com/fastify/fastify/issues/2404 for more details 56 | // The type definitions would have to be update to match this. 57 | const camelCase = toCamelCase(options.name) 58 | if (!autoName && !fn[camelCase]) { 59 | fn[camelCase] = fn 60 | } 61 | 62 | return fn 63 | } 64 | 65 | module.exports = plugin 66 | module.exports.default = plugin 67 | module.exports.fastifyPlugin = plugin 68 | -------------------------------------------------------------------------------- /test/bundlers.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const fp = require('../plugin') 5 | 6 | test('webpack removes require.main.filename', t => { 7 | const filename = require.main.filename 8 | const info = console.info 9 | t.after(() => { 10 | require.main.filename = filename 11 | console.info = info 12 | }) 13 | 14 | require.main.filename = null 15 | 16 | console.info = function (msg) { 17 | t.assert.fail('logged: ' + msg) 18 | } 19 | 20 | fp((_fastify, _opts, next) => { 21 | next() 22 | }, { 23 | fastify: '^5.0.0' 24 | }) 25 | }) 26 | 27 | test('support faux modules', (t) => { 28 | const plugin = fp((_fastify, _opts, next) => { 29 | next() 30 | }) 31 | 32 | t.assert.strictEqual(plugin.default, plugin) 33 | }) 34 | 35 | test('support faux modules does not override existing default field in babel module', (t) => { 36 | const module = { 37 | default: (_fastify, _opts, next) => next() 38 | } 39 | 40 | module.default.default = 'Existing default field' 41 | 42 | const plugin = fp(module) 43 | 44 | t.assert.strictEqual(plugin.default, 'Existing default field') 45 | }) 46 | 47 | test('support ts named imports', (t) => { 48 | const plugin = fp((_fastify, _opts, next) => { 49 | next() 50 | }, { 51 | name: 'hello' 52 | }) 53 | 54 | t.assert.strictEqual(plugin.hello, plugin) 55 | }) 56 | 57 | test('from kebab-case to camelCase', (t) => { 58 | const plugin = fp((_fastify, _opts, next) => { 59 | next() 60 | }, { 61 | name: 'hello-world' 62 | }) 63 | 64 | t.assert.strictEqual(plugin.helloWorld, plugin) 65 | }) 66 | 67 | test('from @-prefixed named imports', (t) => { 68 | const plugin = fp((_fastify, _opts, next) => { 69 | next() 70 | }, { 71 | name: '@hello/world' 72 | }) 73 | 74 | t.assert.strictEqual(plugin.helloWorld, plugin) 75 | }) 76 | 77 | test('from @-prefixed named kebab-case to camelCase', (t) => { 78 | const plugin = fp((_fastify, _opts, next) => { 79 | next() 80 | }, { 81 | name: '@hello/my-world' 82 | }) 83 | 84 | t.assert.strictEqual(plugin.helloMyWorld, plugin) 85 | }) 86 | 87 | test('from kebab-case to camelCase multiple words', (t) => { 88 | const plugin = fp((_fastify, _opts, next) => { 89 | next() 90 | }, { 91 | name: 'hello-long-world' 92 | }) 93 | 94 | t.assert.strictEqual(plugin.helloLongWorld, plugin) 95 | }) 96 | 97 | test('from kebab-case to camelCase multiple words does not override', (t) => { 98 | const fn = (_fastify, _opts, next) => { 99 | next() 100 | } 101 | 102 | const foobar = {} 103 | fn.helloLongWorld = foobar 104 | 105 | const plugin = fp(fn, { 106 | name: 'hello-long-world' 107 | }) 108 | 109 | t.assert.strictEqual(plugin.helloLongWorld, foobar) 110 | }) 111 | -------------------------------------------------------------------------------- /test/checkVersion.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const fp = require('../plugin') 5 | 6 | test('checkVersion having require.main.filename', (t) => { 7 | const info = console.info 8 | t.assert.ok(require.main.filename) 9 | t.after(() => { 10 | console.info = info 11 | }) 12 | 13 | console.info = function (msg) { 14 | t.assert.fail('logged: ' + msg) 15 | } 16 | 17 | fp((_fastify, _opts, next) => { 18 | next() 19 | }, { 20 | fastify: '^5.0.0' 21 | }) 22 | }) 23 | 24 | test('checkVersion having no require.main.filename but process.argv[1]', (t) => { 25 | const filename = require.main.filename 26 | const info = console.info 27 | t.after(() => { 28 | require.main.filename = filename 29 | console.info = info 30 | }) 31 | 32 | require.main.filename = null 33 | 34 | console.info = function (msg) { 35 | t.assert.fail('logged: ' + msg) 36 | } 37 | 38 | fp((_fastify, _opts, next) => { 39 | next() 40 | }, { 41 | fastify: '^5.0.0' 42 | }) 43 | }) 44 | 45 | test('checkVersion having no require.main.filename and no process.argv[1]', (t) => { 46 | const filename = require.main.filename 47 | const argv = process.argv 48 | const info = console.info 49 | t.after(() => { 50 | require.main.filename = filename 51 | process.argv = argv 52 | console.info = info 53 | }) 54 | 55 | require.main.filename = null 56 | process.argv[1] = null 57 | 58 | console.info = function (msg) { 59 | t.assert.fail('logged: ' + msg) 60 | } 61 | 62 | fp((_fastify, _opts, next) => { 63 | next() 64 | }, { 65 | fastify: '^5.0.0' 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /test/composite.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const fp = require('../plugin') 5 | 6 | test('anonymous function should be named composite.test0', (t) => { 7 | t.plan(2) 8 | const fn = fp((_fastify, _opts, next) => { 9 | next() 10 | }) 11 | 12 | t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'composite.test-auto-0') 13 | t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'composite.test-auto-0') 14 | }) 15 | -------------------------------------------------------------------------------- /test/esm/esm.mjs: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test' 2 | import fp from '../../plugin.js' 3 | 4 | test('esm base support', (t) => { 5 | fp((_fastify, _opts, next) => { 6 | next() 7 | }, { 8 | fastify: '^5.0.0' 9 | }) 10 | t.assert.ok(true, 'fp function called without throwing an error') 11 | }) 12 | -------------------------------------------------------------------------------- /test/esm/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Node v8 throw a `SyntaxError: Unexpected token import` 4 | // even if this branch is never touch in the code, 5 | // by using `eval` we can avoid this issue. 6 | // eslint-disable-next-line 7 | new Function('module', 'return import(module)')('./esm.mjs').catch((err) => { 8 | process.nextTick(() => { 9 | throw err 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /test/extractPluginName.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const extractPluginName = require('../lib/getPluginName').extractPluginName 5 | 6 | const winStack = `Error: anonymous function 7 | at checkName (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\index.js:43:11) 8 | at plugin (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\index.js:24:20) 9 | at Test.test (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\test\\hello.test.js:9:14) 10 | at bound (domain.js:396:14) 11 | at Test.runBound (domain.js:409:12) 12 | at ret (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:278:21) 13 | at Test.main (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:282:7) 14 | at writeSubComment (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:371:13) 15 | at TAP.writeSubComment (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:403:5) 16 | at Test.runBeforeEach (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:370:14) 17 | at loop (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\function-loop\\index.js:35:15) 18 | at TAP.runBeforeEach (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:683:7) 19 | at TAP.processSubtest (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:369:12) 20 | at TAP.process (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:306:14) 21 | at TAP.sub (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:185:10) 22 | at TAP.test (C:\\Users\\leonardo.davinci\\Desktop\\fastify-plugin\\node_modules\\tap\\lib\\test.js:209:17)` 23 | 24 | const nixStack = `Error: anonymous function 25 | at checkName (/home/leonardo/desktop/fastify-plugin/index.js:43:11) 26 | at plugin (/home/leonardo/desktop/fastify-plugin/index.js:24:20) 27 | at Test.test (/home/leonardo/desktop/fastify-plugin/test/this.is.a.test.js:9:14) 28 | at bound (domain.js:396:14) 29 | at Test.runBound (domain.js:409:12) 30 | at ret (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:278:21) 31 | at Test.main (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:282:7) 32 | at writeSubComment (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:371:13) 33 | at TAP.writeSubComment (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:403:5) 34 | at Test.runBeforeEach (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:370:14) 35 | at loop (/home/leonardo/desktop/fastify-plugin/node_modules/function-loop/index.js:35:15) 36 | at TAP.runBeforeEach (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:683:7) 37 | at TAP.processSubtest (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:369:12) 38 | at TAP.process (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:306:14) 39 | at TAP.sub (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:185:10) 40 | at TAP.test (/home/leonardo/desktop/fastify-plugin/node_modules/tap/lib/test.js:209:17)` 41 | 42 | const anonymousStack = 'Unable to parse this' 43 | 44 | test('extractPluginName tests', (t) => { 45 | t.plan(3) 46 | t.assert.strictEqual(extractPluginName(winStack), 'hello.test') 47 | t.assert.strictEqual(extractPluginName(nixStack), 'this.is.a.test') 48 | t.assert.strictEqual(extractPluginName(anonymousStack), 'anonymous') 49 | }) 50 | -------------------------------------------------------------------------------- /test/mu1tip1e.composite.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const fp = require('../plugin') 5 | 6 | test('anonymous function should be named mu1tip1e.composite.test', (t) => { 7 | t.plan(2) 8 | 9 | const fn = fp((_fastify, _opts, next) => { 10 | next() 11 | }) 12 | 13 | t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'mu1tip1e.composite.test-auto-0') 14 | t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'mu1tip1e.composite.test-auto-0') 15 | }) 16 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const proxyquire = require('proxyquire') 5 | const fp = require('../plugin') 6 | const Fastify = require('fastify') 7 | 8 | const pkg = require('../package.json') 9 | 10 | test('fastify-plugin is a function', (t) => { 11 | t.plan(1) 12 | t.assert.ok(typeof fp === 'function') 13 | }) 14 | 15 | test('should return the function with the skip-override Symbol', (t) => { 16 | t.plan(1) 17 | 18 | function plugin (_fastify, _opts, next) { 19 | next() 20 | } 21 | 22 | fp(plugin) 23 | t.assert.ok(plugin[Symbol.for('skip-override')]) 24 | }) 25 | 26 | test('should support "default" function from babel module', (t) => { 27 | t.plan(1) 28 | 29 | const plugin = { 30 | default: () => { } 31 | } 32 | 33 | try { 34 | fp(plugin) 35 | t.assert.ok(true) 36 | } catch (e) { 37 | t.assert.strictEqual(e.message, 'fastify-plugin expects a function, instead got a \'object\'') 38 | } 39 | }) 40 | 41 | test('should throw if the plugin is not a function', (t) => { 42 | t.plan(1) 43 | 44 | try { 45 | fp('plugin') 46 | t.assert.fail() 47 | } catch (e) { 48 | t.assert.strictEqual(e.message, 'fastify-plugin expects a function, instead got a \'string\'') 49 | } 50 | }) 51 | 52 | test('should check the fastify version', (t) => { 53 | t.plan(1) 54 | 55 | function plugin (_fastify, _opts, next) { 56 | next() 57 | } 58 | 59 | try { 60 | fp(plugin, { fastify: '>=0.10.0' }) 61 | t.assert.ok(true) 62 | } catch { 63 | t.assert.fail() 64 | } 65 | }) 66 | 67 | test('should check the fastify version', (t) => { 68 | t.plan(1) 69 | 70 | function plugin (_fastify, _opts, next) { 71 | next() 72 | } 73 | 74 | try { 75 | fp(plugin, '>=0.10.0') 76 | t.assert.ok(true) 77 | } catch { 78 | t.assert.fail() 79 | } 80 | }) 81 | 82 | test('the options object should be an object', (t) => { 83 | t.plan(2) 84 | 85 | try { 86 | fp(() => { }, null) 87 | t.assert.fail() 88 | } catch (e) { 89 | t.assert.strictEqual(e.message, 'The options object should be an object') 90 | } 91 | 92 | try { 93 | fp(() => { }, []) 94 | t.assert.fail() 95 | } catch (e) { 96 | t.assert.strictEqual(e.message, 'The options object should be an object') 97 | } 98 | }) 99 | 100 | test('should throw if the version number is not a string', (t) => { 101 | t.plan(1) 102 | 103 | try { 104 | fp(() => { }, { fastify: 12 }) 105 | t.assert.fail() 106 | } catch (e) { 107 | t.assert.strictEqual(e.message, 'fastify-plugin expects a version string, instead got \'number\'') 108 | } 109 | }) 110 | 111 | test('Should accept an option object', (t) => { 112 | t.plan(2) 113 | 114 | const opts = { hello: 'world' } 115 | 116 | function plugin (_fastify, _opts, next) { 117 | next() 118 | } 119 | 120 | fp(plugin, opts) 121 | 122 | t.assert.ok(plugin[Symbol.for('skip-override')], 'skip-override symbol should be present') 123 | t.assert.deepStrictEqual(plugin[Symbol.for('plugin-meta')], opts, 'plugin-meta should match opts') 124 | }) 125 | 126 | test('Should accept an option object and checks the version', (t) => { 127 | t.plan(2) 128 | 129 | const opts = { hello: 'world', fastify: '>=0.10.0' } 130 | 131 | function plugin (_fastify, _opts, next) { 132 | next() 133 | } 134 | 135 | fp(plugin, opts) 136 | t.assert.ok(plugin[Symbol.for('skip-override')]) 137 | t.assert.deepStrictEqual(plugin[Symbol.for('plugin-meta')], opts) 138 | }) 139 | 140 | test('should set anonymous function name to file it was called from with a counter', (t) => { 141 | const fp = proxyquire('../plugin.js', { stubs: {} }) 142 | 143 | const fn = fp((_fastify, _opts, next) => { 144 | next() 145 | }) 146 | 147 | t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'test-auto-0') 148 | t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'test-auto-0') 149 | 150 | const fn2 = fp((_fastify, _opts, next) => { 151 | next() 152 | }) 153 | 154 | t.assert.strictEqual(fn2[Symbol.for('plugin-meta')].name, 'test-auto-1') 155 | t.assert.strictEqual(fn2[Symbol.for('fastify.display-name')], 'test-auto-1') 156 | }) 157 | 158 | test('should set function name if Error.stackTraceLimit is set to 0', (t) => { 159 | const stackTraceLimit = Error.stackTraceLimit = 0 160 | 161 | const fp = proxyquire('../plugin.js', { stubs: {} }) 162 | 163 | const fn = fp((_fastify, _opts, next) => { 164 | next() 165 | }) 166 | 167 | t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'test-auto-0') 168 | t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'test-auto-0') 169 | 170 | const fn2 = fp((_fastify, _opts, next) => { 171 | next() 172 | }) 173 | 174 | t.assert.strictEqual(fn2[Symbol.for('plugin-meta')].name, 'test-auto-1') 175 | t.assert.strictEqual(fn2[Symbol.for('fastify.display-name')], 'test-auto-1') 176 | 177 | Error.stackTraceLimit = stackTraceLimit 178 | }) 179 | 180 | test('should set display-name to meta name', (t) => { 181 | t.plan(2) 182 | 183 | const functionName = 'superDuperSpecialFunction' 184 | 185 | const fn = fp((_fastify, _opts, next) => next(), { 186 | name: functionName 187 | }) 188 | 189 | t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, functionName) 190 | t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], functionName) 191 | }) 192 | 193 | test('should preserve fastify version in meta', (t) => { 194 | t.plan(1) 195 | 196 | const opts = { hello: 'world', fastify: '>=0.10.0' } 197 | 198 | const fn = fp((_fastify, _opts, next) => next(), opts) 199 | 200 | t.assert.strictEqual(fn[Symbol.for('plugin-meta')].fastify, '>=0.10.0') 201 | }) 202 | 203 | test('should check fastify dependency graph - plugin', async (t) => { 204 | t.plan(1) 205 | const fastify = Fastify() 206 | 207 | fastify.register(fp((_fastify, _opts, next) => next(), { 208 | fastify: '5.x', 209 | name: 'plugin1-name' 210 | })) 211 | 212 | fastify.register(fp((_fastify, _opts, next) => next(), { 213 | fastify: '5.x', 214 | name: 'test', 215 | dependencies: ['plugin1-name', 'plugin2-name'] 216 | })) 217 | 218 | await t.assert.rejects(fastify.ready(), { message: "The dependency 'plugin2-name' of plugin 'test' is not registered" }) 219 | }) 220 | 221 | test('should check fastify dependency graph - decorate', async (t) => { 222 | t.plan(1) 223 | const fastify = Fastify() 224 | 225 | fastify.decorate('plugin1', fp((_fastify, _opts, next) => next(), { 226 | fastify: '5.x', 227 | name: 'plugin1-name' 228 | })) 229 | 230 | fastify.register(fp((_fastify, _opts, next) => next(), { 231 | fastify: '5.x', 232 | name: 'test', 233 | decorators: { fastify: ['plugin1', 'plugin2'] } 234 | })) 235 | 236 | await t.assert.rejects(fastify.ready(), { message: "The decorator 'plugin2' required by 'test' is not present in Fastify" }) 237 | }) 238 | 239 | test('should check fastify dependency graph - decorateReply', async (t) => { 240 | t.plan(1) 241 | const fastify = Fastify() 242 | 243 | fastify.decorateReply('plugin1', fp((_fastify, _opts, next) => next(), { 244 | fastify: '5.x', 245 | name: 'plugin1-name' 246 | })) 247 | 248 | fastify.register(fp((_fastify, _opts, next) => next(), { 249 | fastify: '5.x', 250 | name: 'test', 251 | decorators: { reply: ['plugin1', 'plugin2'] } 252 | })) 253 | 254 | await t.assert.rejects(fastify.ready(), { message: "The decorator 'plugin2' required by 'test' is not present in Reply" }) 255 | }) 256 | 257 | test('should accept an option to encapsulate', async (t) => { 258 | t.plan(3) 259 | 260 | const fastify = Fastify() 261 | 262 | fastify.register(fp((fastify, _opts, next) => { 263 | fastify.decorate('accessible', true) 264 | next() 265 | }, { 266 | name: 'accessible-plugin' 267 | })) 268 | 269 | fastify.register(fp((fastify, _opts, next) => { 270 | fastify.decorate('alsoAccessible', true) 271 | next() 272 | }, { 273 | name: 'accessible-plugin2', 274 | encapsulate: false 275 | })) 276 | 277 | fastify.register(fp((fastify, _opts, next) => { 278 | fastify.decorate('encapsulated', true) 279 | next() 280 | }, { 281 | name: 'encapsulated-plugin', 282 | encapsulate: true 283 | })) 284 | 285 | await fastify.ready() 286 | 287 | t.assert.ok(fastify.hasDecorator('accessible')) 288 | t.assert.ok(fastify.hasDecorator('alsoAccessible')) 289 | t.assert.ok(!fastify.hasDecorator('encapsulated')) 290 | }) 291 | 292 | test('should check dependencies when encapsulated', async (t) => { 293 | t.plan(1) 294 | const fastify = Fastify() 295 | 296 | fastify.register(fp((_fastify, _opts, next) => next(), { 297 | name: 'test', 298 | dependencies: ['missing-dependency-name'], 299 | encapsulate: true 300 | })) 301 | 302 | await t.assert.rejects(fastify.ready(), { message: "The dependency 'missing-dependency-name' of plugin 'test' is not registered" }) 303 | }) 304 | 305 | test( 306 | 'should check version when encapsulated', 307 | { skip: /\d-.+/.test(pkg.devDependencies.fastify) }, 308 | async (t) => { 309 | t.plan(1) 310 | const fastify = Fastify() 311 | 312 | fastify.register(fp((_fastify, _opts, next) => next(), { 313 | name: 'test', 314 | fastify: '<=2.10.0', 315 | encapsulate: true 316 | })) 317 | 318 | await t.assert.rejects(fastify.ready(), { message: /fastify-plugin: test - expected '<=2.10.0' fastify version, '\d.\d+.\d+' is installed/ }) 319 | } 320 | ) 321 | 322 | test('should check decorators when encapsulated', async (t) => { 323 | t.plan(1) 324 | const fastify = Fastify() 325 | 326 | fastify.decorate('plugin1', 'foo') 327 | 328 | fastify.register(fp((_fastify, _opts, next) => next(), { 329 | fastify: '5.x', 330 | name: 'test', 331 | encapsulate: true, 332 | decorators: { fastify: ['plugin1', 'plugin2'] } 333 | })) 334 | 335 | await t.assert.rejects(fastify.ready(), { message: "The decorator 'plugin2' required by 'test' is not present in Fastify" }) 336 | }) 337 | 338 | test('plugin name when encapsulated', async (t) => { 339 | t.plan(6) 340 | const fastify = Fastify() 341 | 342 | fastify.register(function plugin (_instance, _opts, next) { 343 | next() 344 | }) 345 | 346 | fastify.register(fp(getFn('hello'), { 347 | fastify: '5.x', 348 | name: 'hello', 349 | encapsulate: true 350 | })) 351 | 352 | fastify.register(function plugin (fastify, _opts, next) { 353 | fastify.register(fp(getFn('deep'), { 354 | fastify: '5.x', 355 | name: 'deep', 356 | encapsulate: true 357 | })) 358 | 359 | fastify.register(fp(function genericPlugin (fastify, _opts, next) { 360 | t.assert.strictEqual(fastify.pluginName, 'deep-deep', 'should be deep-deep') 361 | 362 | fastify.register(fp(getFn('deep-deep-deep'), { 363 | fastify: '5.x', 364 | name: 'deep-deep-deep', 365 | encapsulate: true 366 | })) 367 | 368 | fastify.register(fp(getFn('deep-deep -> not-encapsulated-2'), { 369 | fastify: '5.x', 370 | name: 'not-encapsulated-2' 371 | })) 372 | 373 | next() 374 | }, { 375 | fastify: '5.x', 376 | name: 'deep-deep', 377 | encapsulate: true 378 | })) 379 | 380 | fastify.register(fp(getFn('plugin -> not-encapsulated'), { 381 | fastify: '5.x', 382 | name: 'not-encapsulated' 383 | })) 384 | 385 | next() 386 | }) 387 | 388 | await fastify.ready() 389 | 390 | function getFn (expectedName) { 391 | return function genericPlugin (fastify, _opts, next) { 392 | t.assert.strictEqual(fastify.pluginName, expectedName, `should be ${expectedName}`) 393 | next() 394 | } 395 | } 396 | }) 397 | -------------------------------------------------------------------------------- /test/toCamelCase.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const toCamelCase = require('../lib/toCamelCase') 5 | 6 | test('from kebab-case to camelCase', (t) => { 7 | t.plan(1) 8 | t.assert.strictEqual(toCamelCase('hello-world'), 'helloWorld') 9 | }) 10 | 11 | test('from @-prefixed named imports', (t) => { 12 | t.plan(1) 13 | t.assert.strictEqual(toCamelCase('@hello/world'), 'helloWorld') 14 | }) 15 | 16 | test('from @-prefixed named kebab-case to camelCase', (t) => { 17 | t.plan(1) 18 | t.assert.strictEqual(toCamelCase('@hello/my-world'), 'helloMyWorld') 19 | }) 20 | 21 | test('from kebab-case to camelCase multiple words', (t) => { 22 | t.plan(1) 23 | t.assert.strictEqual(toCamelCase('hello-long-world'), 'helloLongWorld') 24 | }) 25 | -------------------------------------------------------------------------------- /types/example-async.test-d.ts: -------------------------------------------------------------------------------- 1 | import { FastifyPluginAsync } from 'fastify' 2 | 3 | type FastifyExampleAsync = FastifyPluginAsync 4 | 5 | declare namespace fastifyExampleAsync { 6 | 7 | export interface FastifyExampleAsyncOptions { 8 | foo?: 'bar' 9 | } 10 | 11 | export interface FastifyExampleAsyncPluginOptions extends FastifyExampleAsyncOptions { 12 | } 13 | export const fastifyExampleAsync: FastifyExampleAsync 14 | export { fastifyExampleAsync as default } 15 | } 16 | 17 | declare function fastifyExampleAsync (...params: Parameters): ReturnType 18 | 19 | export default fastifyExampleAsync 20 | -------------------------------------------------------------------------------- /types/example-callback.test-d.ts: -------------------------------------------------------------------------------- 1 | import { FastifyPluginCallback } from 'fastify' 2 | 3 | type FastifyExampleCallback = FastifyPluginCallback 4 | 5 | declare namespace fastifyExampleCallback { 6 | 7 | export interface FastifyExampleCallbackOptions { 8 | foo?: 'bar' 9 | } 10 | 11 | export interface FastifyExampleCallbackPluginOptions extends FastifyExampleCallbackOptions { 12 | } 13 | export const fastifyExampleCallback: FastifyExampleCallback 14 | export { fastifyExampleCallback as default } 15 | } 16 | 17 | declare function fastifyExampleCallback (...params: Parameters): ReturnType 18 | 19 | export default fastifyExampleCallback 20 | -------------------------------------------------------------------------------- /types/plugin.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { 4 | FastifyPluginCallback, 5 | FastifyPluginAsync, 6 | FastifyPluginOptions, 7 | RawServerBase, 8 | RawServerDefault, 9 | FastifyTypeProvider, 10 | FastifyTypeProviderDefault, 11 | FastifyBaseLogger, 12 | } from 'fastify' 13 | 14 | type FastifyPlugin = typeof fastifyPlugin 15 | 16 | declare namespace fastifyPlugin { 17 | export interface PluginMetadata { 18 | /** Bare-minimum version of Fastify for your plugin, just add the semver range that you need. */ 19 | fastify?: string, 20 | name?: string, 21 | /** Decorator dependencies for this plugin */ 22 | decorators?: { 23 | fastify?: (string | symbol)[], 24 | reply?: (string | symbol)[], 25 | request?: (string | symbol)[] 26 | }, 27 | /** The plugin dependencies */ 28 | dependencies?: string[], 29 | encapsulate?: boolean 30 | } 31 | // Exporting PluginOptions for backward compatibility after renaming it to PluginMetadata 32 | /** 33 | * @deprecated Use PluginMetadata instead 34 | */ 35 | export interface PluginOptions extends PluginMetadata {} 36 | 37 | export const fastifyPlugin: FastifyPlugin 38 | export { fastifyPlugin as default } 39 | } 40 | 41 | /** 42 | * This function does three things for you: 43 | * 1. Add the `skip-override` hidden property 44 | * 2. Check bare-minimum version of Fastify 45 | * 3. Pass some custom metadata of the plugin to Fastify 46 | * @param fn Fastify plugin function 47 | * @param options Optional plugin options 48 | */ 49 | 50 | declare function fastifyPlugin< 51 | Options extends FastifyPluginOptions = Record, 52 | RawServer extends RawServerBase = RawServerDefault, 53 | TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault, 54 | Logger extends FastifyBaseLogger = FastifyBaseLogger, 55 | Fn extends FastifyPluginCallback | FastifyPluginAsync = FastifyPluginCallback 56 | > ( 57 | fn: Fn extends unknown ? Fn extends (...args: any) => Promise ? FastifyPluginAsync : FastifyPluginCallback : Fn, 58 | options?: fastifyPlugin.PluginMetadata | string 59 | ): Fn 60 | 61 | export = fastifyPlugin 62 | -------------------------------------------------------------------------------- /types/plugin.test-d.ts: -------------------------------------------------------------------------------- 1 | import fastifyPlugin from '..' 2 | import fastify, { FastifyPluginCallback, FastifyPluginAsync, FastifyError, FastifyInstance, FastifyPluginOptions, RawServerDefault, FastifyTypeProviderDefault, FastifyBaseLogger } from 'fastify' 3 | import { expectAssignable, expectError, expectNotType, expectType } from 'tsd' 4 | import { Server } from 'node:https' 5 | import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox' 6 | import fastifyExampleCallback from './example-callback.test-d' 7 | import fastifyExampleAsync from './example-async.test-d' 8 | 9 | interface Options { 10 | foo: string 11 | } 12 | 13 | const testSymbol = Symbol('foobar') 14 | 15 | // Callback 16 | 17 | const pluginCallback: FastifyPluginCallback = (_fastify, _options, _next) => { } 18 | expectType(fastifyPlugin(pluginCallback)) 19 | 20 | const pluginCallbackWithTypes = (_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { } 21 | expectAssignable(fastifyPlugin(pluginCallbackWithTypes)) 22 | expectNotType(fastifyPlugin(pluginCallbackWithTypes)) 23 | 24 | expectAssignable(fastifyPlugin((_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { })) 25 | expectNotType(fastifyPlugin((_fastify: FastifyInstance, _options: FastifyPluginOptions, _next: (error?: FastifyError) => void): void => { })) 26 | 27 | expectType(fastifyPlugin(pluginCallback, '')) 28 | expectType(fastifyPlugin(pluginCallback, { 29 | fastify: '', 30 | name: '', 31 | decorators: { 32 | fastify: ['', testSymbol], 33 | reply: ['', testSymbol], 34 | request: ['', testSymbol] 35 | }, 36 | dependencies: [''], 37 | encapsulate: true 38 | })) 39 | 40 | const pluginCallbackWithOptions: FastifyPluginCallback = (_fastify, options, _next) => { 41 | expectType(options.foo) 42 | } 43 | 44 | expectType>(fastifyPlugin(pluginCallbackWithOptions)) 45 | 46 | const pluginCallbackWithServer: FastifyPluginCallback = (fastify, _options, _next) => { 47 | expectType(fastify.server) 48 | } 49 | 50 | expectType>(fastifyPlugin(pluginCallbackWithServer)) 51 | 52 | const pluginCallbackWithTypeProvider: FastifyPluginCallback = (_fastify, _options, _next) => { } 53 | 54 | expectType>(fastifyPlugin(pluginCallbackWithTypeProvider)) 55 | 56 | // Async 57 | 58 | const pluginAsync: FastifyPluginAsync = async (_fastify, _options) => { } 59 | expectType(fastifyPlugin(pluginAsync)) 60 | 61 | const pluginAsyncWithTypes = async (_fastify: FastifyInstance, _options: FastifyPluginOptions): Promise => { } 62 | expectType>(fastifyPlugin(pluginAsyncWithTypes)) 63 | 64 | expectType>(fastifyPlugin(async (_fastify: FastifyInstance, _options: FastifyPluginOptions): Promise => { })) 65 | expectType(fastifyPlugin(pluginAsync, '')) 66 | expectType(fastifyPlugin(pluginAsync, { 67 | fastify: '', 68 | name: '', 69 | decorators: { 70 | fastify: ['', testSymbol], 71 | reply: ['', testSymbol], 72 | request: ['', testSymbol] 73 | }, 74 | dependencies: [''], 75 | encapsulate: true 76 | })) 77 | 78 | const pluginAsyncWithOptions: FastifyPluginAsync = async (_fastify, options) => { 79 | expectType(options.foo) 80 | } 81 | 82 | expectType>(fastifyPlugin(pluginAsyncWithOptions)) 83 | 84 | const pluginAsyncWithServer: FastifyPluginAsync = async (fastify, _options) => { 85 | expectType(fastify.server) 86 | } 87 | 88 | expectType>(fastifyPlugin(pluginAsyncWithServer)) 89 | 90 | const pluginAsyncWithTypeProvider: FastifyPluginAsync = async (_fastify, _options) => { } 91 | 92 | expectType>(fastifyPlugin(pluginAsyncWithTypeProvider)) 93 | 94 | // Fastify register 95 | 96 | const server = fastify() 97 | server.register(fastifyPlugin(pluginCallback)) 98 | server.register(fastifyPlugin(pluginCallbackWithTypes), { foo: 'bar' }) 99 | server.register(fastifyPlugin(pluginCallbackWithOptions), { foo: 'bar' }) 100 | server.register(fastifyPlugin(pluginCallbackWithServer), { foo: 'bar' }) 101 | server.register(fastifyPlugin(pluginCallbackWithTypeProvider), { foo: 'bar' }) 102 | server.register(fastifyPlugin(pluginAsync)) 103 | server.register(fastifyPlugin(pluginAsyncWithTypes), { foo: 'bar' }) 104 | server.register(fastifyPlugin(pluginAsyncWithOptions), { foo: 'bar' }) 105 | server.register(fastifyPlugin(pluginAsyncWithServer), { foo: 'bar' }) 106 | server.register(fastifyPlugin(pluginAsyncWithTypeProvider), { foo: 'bar' }) 107 | 108 | // properly handling callback and async 109 | fastifyPlugin(function (fastify, options, next) { 110 | expectType(fastify) 111 | expectType>(options) 112 | expectType<(err?: Error) => void>(next) 113 | }) 114 | 115 | fastifyPlugin(function (fastify, options, next) { 116 | expectType(fastify) 117 | expectType(options) 118 | expectType<(err?: Error) => void>(next) 119 | }) 120 | 121 | fastifyPlugin(async function (fastify, options) { 122 | expectType(fastify) 123 | expectType(options) 124 | }) 125 | 126 | expectAssignable>(fastifyPlugin(async function (_fastify: FastifyInstance, _options: Options) { })) 127 | expectNotType(fastifyPlugin(async function (_fastify: FastifyInstance, _options: Options) { })) 128 | 129 | fastifyPlugin(async function (fastify, options: Options) { 130 | expectType(fastify) 131 | expectType(options) 132 | }) 133 | 134 | fastifyPlugin(async function (fastify, options) { 135 | expectType(fastify) 136 | expectType>(options) 137 | }) 138 | 139 | expectError( 140 | fastifyPlugin(async function (fastify, options: Options, _next) { 141 | expectType(fastify) 142 | expectType(options) 143 | }) 144 | ) 145 | expectAssignable>(fastifyPlugin(function (_fastify, _options, _next) { })) 146 | expectNotType(fastifyPlugin(function (_fastify, _options, _next) { })) 147 | 148 | fastifyPlugin(function (fastify, options: Options, next) { 149 | expectType(fastify) 150 | expectType(options) 151 | expectType<(err?: Error) => void>(next) 152 | }) 153 | 154 | expectError( 155 | fastifyPlugin(function (fastify, options: Options, _next) { 156 | expectType(fastify) 157 | expectType(options) 158 | return Promise.resolve() 159 | }) 160 | ) 161 | 162 | server.register(fastifyExampleCallback, { foo: 'bar' }) 163 | expectError(server.register(fastifyExampleCallback, { foo: 'baz' })) 164 | 165 | server.register(fastifyExampleAsync, { foo: 'bar' }) 166 | expectError(server.register(fastifyExampleAsync, { foo: 'baz' })) 167 | --------------------------------------------------------------------------------