UNPKG

@fastify/autoload

Version:
635 lines (458 loc) 17.2 kB
# @fastify/autoload [![CI](https://github.com/fastify/fastify-autoload/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-autoload/actions/workflows/ci.yml) [![NPM version](https://img.shields.io/npm/v/@fastify/autoload.svg?style=flat)](https://www.npmjs.com/package/@fastify/autoload) [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) Convenience plugin for Fastify that loads all plugins found in a directory and automatically configures routes matching the folder structure. ## Installation ``` npm i @fastify/autoload ``` ### Compatibility | Plugin version | Fastify version | | ---------------|-----------------| | `>=6.x` | `^5.x` | | `^5.x` | `^4.x` | | `>=2.x <5.x` | `^3.x` | | `^1.x` | `^2.x` | | `^1.x` | `^1.x` | Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin in the table above. See [Fastify's LTS policy](https://github.com/fastify/fastify/blob/main/docs/Reference/LTS.md) for more details. ## Example Fastify server that automatically loads in all plugins from the `plugins` directory: ```js const fastify = require('fastify') const autoload = require('@fastify/autoload') const app = fastify() app.register(autoload, { dir: path.join(__dirname, 'plugins') }) app.listen({ port: 3000 }) ``` or with ESM syntax: ```js import autoLoad from '@fastify/autoload' import { fileURLToPath } from 'node:url' import { dirname, join } from 'node:path' import fastify from 'fastify' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) const app = fastify() app.register(autoLoad, { dir: join(__dirname, 'plugins') }) app.listen({ port: 3000 }) ``` Folder structure: ``` ├── plugins │ ├── hooked-plugin │ │ ├── autohooks.mjs │ │ ├── routes.js │ │ └── children │ │ ├── commonjs.cjs │ │ ├── module.mjs │ │ └── typescript.ts │ ├── single-plugin │ │ ├── index.js │ │ └── utils.js │ ├── more-plugins │ │ ├── commonjs.cjs │ │ ├── module.mjs │ │ └── typescript.ts │ └── another-plugin.js ├── package.json └── app.js ``` ## Global Configuration Autoload can be customized using the following options: ### `dir` (required) Base directory containing plugins to be loaded. Each script file within a directory is treated as a plugin unless the directory contains an index file (e.g. `index.js`). In which case, only the index file (and the potential sub-directories) will be loaded. The following script types are supported: - `.js ` (CommonJS or ES modules depending on `type` field of parent `package.json`) - `.cjs` (CommonJS) - `.mjs` (ES modules) - `.ts` (TypeScript) ### `dirNameRoutePrefix` (optional) - Default: `true` Determines whether routes will be automatically prefixed with the subdirectory name in an autoloaded directory. It can be a sync function that must return a string that will be used as prefix, or it must return `false` to skip the prefix for the directory. ```js fastify.register(autoLoad, { dir: path.join(__dirname, 'routes'), dirNameRoutePrefix: false // lack of prefix will mean no prefix, instead of directory name }) fastify.register(autoLoad, { dir: path.join(__dirname, 'routes'), dirNameRoutePrefix: function rewrite (folderParent, folderName) { if (folderName === 'YELLOW') { return 'yellow-submarine' } if (folderName === 'FoOoO-BaAaR') { return false } return folderName } }) ``` ### `matchFilter` (optional) Filter matching any path that should be loaded. Can be a RegExp, a string, or a function returning a boolean. ```js fastify.register(autoLoad, { dir: path.join(__dirname, 'plugins'), matchFilter: (path) => path.split("/").at(-2) === "handlers" }) ``` ### `ignoreFilter` (optional) Filter matching any path that should not be loaded. Can be a RegExp, a string ,or a function returning a boolean. ```js fastify.register(autoLoad, { dir: path.join(__dirname, 'plugins'), ignoreFilter: (path) => path.endsWith('.spec.js') }) ``` ### `ignorePattern` (optional) RegExp matching any file or folder that should not be loaded. ```js fastify.register(autoLoad, { dir: path.join(__dirname, 'plugins'), ignorePattern: /^.*(?:test|spec).js$/ }) ``` ### `scriptPattern` (optional) Regex to override the script files accepted by default. You should only use this option with a [customization hooks](https://nodejs.org/docs/latest/api/module.html#customization-hooks)provider, such as `ts-node`. Otherwise, widening the acceptance extension here will result in an error. ```js fastify.register(autoLoad, { dir: path.join(__dirname, 'plugins'), scriptPattern: /(?<!\.d)\.(ts|tsx)$/ }) ``` ### `indexPattern` (optional) Regex to override the `index.js` naming convention. ```js fastify.register(autoLoad, { dir: path.join(__dirname, 'plugins'), indexPattern: /^.*routes(?:\.ts|\.js|\.cjs|\.mjs)$/ }) ``` ### `maxDepth` (optional) Limits the depth at which nested plugins are loaded. ```js fastify.register(autoLoad, { dir: path.join(__dirname, 'plugins'), maxDepth: 2 // files in `opts.dir` nested more than 2 directories deep will be ignored. }) ``` ### `forceESM` (optional) If set to 'true' it always use `await import` to load plugins or hooks. ```js fastify.register(autoLoad, { dir: path.join(__dirname, 'plugins'), forceESM: true }) ``` ### `encapsulate` (optional) - Default: `true` If set to `false` each plugin loaded is wrapped with [fastify-plugin](https://github.com/fastify/fastify-plugin). This allows you to share contexts between plugins and the parent context if needed. For example, if you need to share decorators. Read [this](https://github.com/fastify/fastify/blob/main/docs/Reference/Encapsulation.md#sharing-between-contexts) for more details. ```js fastify.register(autoLoad, { dir: path.join(__dirname, 'plugins'), encapsulate: false }) ``` ### `options` (optional) Global options object used for all registered plugins. Any option specified here will override `plugin.autoConfig` options specified in the plugin itself. When setting both `options.prefix` and `plugin.autoPrefix` they will be concatenated. ```js // index.js fastify.register(autoLoad, { dir: path.join(__dirname, 'plugins'), options: { prefix: '/defaultPrefix' } }) // /plugins/something.js module.exports = function (fastify, opts, next) { // your plugin } module.exports.autoPrefix = '/something' // /plugins/something.mjs export default function (f, opts, next) { f.get('/', (request, reply) => { reply.send({ something: 'else' }) }) next() } export const autoPrefix = '/prefixed' // routes can now be added to /defaultPrefix/something ``` ### `autoHooks` (optional) Apply hooks from `autohooks.js` file(s) to plugins found in folder. Automatic hooks from `autohooks` files will be encapsulated with plugins. If `false`, all `autohooks.js` files will be ignored. ```js fastify.register(autoLoad, { dir: path.join(__dirname, 'plugins'), autoHooks: true // apply hooks to routes in this level }) ``` If `autoHooks` is set, all plugins in the folder will be [encapsulated](https://github.com/fastify/fastify/blob/main/docs/Reference/Encapsulation.md) and decorated values _will not be exported_ outside the folder. ### `autoHooksPattern` (optional) Regex to override the `autohooks` naming convention. ```js fastify.register(autoLoad, { dir: path.join(__dirname, 'plugins'), autoHooks: true, autoHooksPattern: /^[_.]?auto_?hooks(?:\.js|\.cjs|\.mjs)$/i }) ``` ### `cascadeHooks` (optional) If using `autoHooks`, cascade hooks to all children. Ignored if `autoHooks` is `false`. Default behavior of `autoHooks` is to apply hooks only to the level on which the `autohooks.js` file is found. Setting `cascadeHooks: true` will continue applying the hooks to any children. ```js fastify.register(autoLoad, { dir: path.join(__dirname, 'plugins'), autoHooks: true, // apply hooks to routes in this level, cascadeHooks: true // continue applying hooks to children, starting at this level }) ``` ### `overwriteHooks` (optional) If using `cascadeHooks`, cascade will be reset when a new `autohooks.js` file is encountered. Ignored if `autoHooks` is `false`. Default behavior of `cascadeHooks` is to accumulate hooks as new `autohooks.js` files are discovered and cascade to children. Setting `overwriteHooks: true` will start a new hook cascade when new `autohooks.js` files are encountered. ```js fastify.register(autoLoad, { dir: path.join(__dirname, 'plugins'), autoHooks: true, // apply hooks to routes in this level, cascadeHooks: true, // continue applying hooks to children, starting at this level, overwriteHooks: true // re-start hook cascade when a new `autohooks.js` file is found }) ``` ### `routeParams` (optional) Folders prefixed with `_` will be turned into dynamic route parameters. If you want to use mixed route parameters use a double underscore `__`. ```js /* ├── routes ├── __country-__language │   │ └── actions.js │ └── users │ ├── _id │ │ └── actions.js │ ├── __country-__language │ │ └── actions.js │ └── index.js └── app.js */ fastify.register(autoLoad, { dir: path.join(__dirname, 'routes'), routeParams: true // routes/users/_id/actions.js will be loaded with prefix /users/:id // routes/__country-__language/actions.js will be loaded with prefix /:country-:language }) // curl http://localhost:3000/users/index // { userIndex: [ { id: 7, username: 'example' } ] } // curl http://localhost:3000/users/7/details // { user: { id: 7, username: 'example' } } // curl http://localhost:3000/be-nl // { country: 'be', language: 'nl' } ``` ## Override TypeScript detection using an environment variable This plugin uses [native type stripping](https://nodejs.org/docs/latest-v23.x/api/typescript.html#modules-typescript) with Node 23 and later. It is possible to override the automatic detection of a TypeScript-capable runtime using the `FASTIFY_AUTOLOAD_TYPESCRIPT` environment variable. If set to a truthy value Autoload will load `.ts` files, expecting that node has a TypeScript-capable loader. This is useful for cases where you want to use Autoload for loading TypeScript files but detecting the TypeScript loader fails because, for example, you are using a custom loader. It can be used like this: ```sh FASTIFY_AUTOLOAD_TYPESCRIPT=1 node --loader=my-custom-loader index.ts ``` ## Plugin Configuration Each plugin can be individually configured using the following module properties: ### `plugin.autoConfig` Specifies the options to be used as the `opts` parameter. ```js module.exports = function (fastify, opts, next) { console.log(opts.foo) // 'bar' next() } module.exports.autoConfig = { foo: 'bar' } ``` Or with ESM syntax: ```js import plugin from '../lib-plugin.js' export default async function myPlugin (app, options) { app.get('/', async (request, reply) => { return { hello: options.name } }) } export const autoConfig = { name: 'y' } ``` You can also use a callback function if you need to access the parent instance: ```js export const autoConfig = (fastify) => { return { name: 'y ' + fastify.rootName } } ``` However, note that the `prefix` option should be set directly on `autoConfig` for autoloading to work as expected: ```js export const autoConfig = (fastify) => { return { name: 'y ' + fastify.rootName } } autoConfig.prefix = '/hello' ``` ### `plugin.autoPrefix` Set routing prefix for plugin. ```js module.exports = function (fastify, opts, next) { fastify.get('/', (request, reply) => { reply.send({ hello: 'world' }) }) next() } module.exports.autoPrefix = '/something' // when loaded with autoload, this will be exposed as /something ``` Or with ESM syntax: ```js export default async function (app, opts) { app.get('/', (request, reply) => { return { something: 'else' } }) } export const autoPrefix = '/prefixed' ``` ### `plugin.prefixOverride` Override all other prefix options. ```js // index.js fastify.register(autoLoad, { dir: path.join(__dirname, 'plugins'), options: { prefix: '/defaultPrefix' } }) // /foo/something.js module.exports = function (fastify, opts, next) { // your plugin } module.exports.prefixOverride = '/overriddenPrefix' // this will be exposed as /overriddenPrefix ``` Or with ESM syntax: ```js export default async function (app, opts) { // your plugin } export const prefixOverride = '/overriddenPrefix' ``` If you have a plugin in the folder you do not want any prefix applied to, you can set `prefixOverride = ''`: ```js // index.js fastify.register(autoLoad, { dir: path.join(__dirname, 'plugins'), options: { prefix: '/defaultPrefix' } }) // /foo/something.js module.exports = function (fastify, opts, next) { // your plugin } // optional module.exports.prefixOverride = '' // routes can now be added without a prefix ``` ### `plugin.autoload` Toggle whether the plugin should be loaded. Example: ```js module.exports = function (fastify, opts, next) { // your plugin } // optional module.exports.autoload = false ``` ### `opts.name` Set name of plugin so that it can be referenced as a dependency. ### `opts.dependencies` Set plugin dependencies to ensure correct load order. Example: ```js // plugins/plugin-a.js const fp = require('fastify-plugin') function plugin (fastify, opts, next) { // plugin a } module.exports = fp(plugin, { name: 'plugin-a', dependencies: ['plugin-b'] }) // plugins/plugin-b.js function plugin (fastify, opts, next) { // plugin b } module.exports = fp(plugin, { name: 'plugin-b' }) ``` ## Autohooks: The autohooks functionality provides several options for automatically adding hooks, decorators, etc. to your routes. CJS and ESM `autohook` formats are supported. The default behavior of `autoHooks: true` is to encapsulate the `autohooks.js` plugin with the contents of the folder containing the file. The `cascadeHooks: true` option encapsulates the hooks with the current folder contents and all subsequent children, with any additional `autohooks.js` files being applied cumulatively. The `overwriteHooks: true` option will restart the cascade any time an `autohooks.js` file is encountered. Plugins and hooks are encapsulated together by folder and registered on the `fastify` instance that loaded the `@fastify/autoload` plugin. For more information on how encapsulation works in Fastify, see: https://fastify.dev/docs/latest/Reference/Encapsulation/#encapsulation ### Example: ``` ├── plugins │ ├── hooked-plugin │ │ ├── autohooks.js // req.hookOne = 'yes' # CJS syntax │ │ ├── routes.js │ │ └── children │ │ ├── old-routes.js │ │ ├── new-routes.js │ │ └── grandchildren │ │ ├── autohooks.mjs // req.hookTwo = 'yes' # ESM syntax │ │ └── routes.mjs │ └── standard-plugin │ └── routes.js └── app.js ``` ```js // hooked-plugin/autohooks.js module.exports = async function (app, opts) { app.addHook('onRequest', async (req, reply) => { req.hookOne = yes; }); } // hooked-plugin/children/grandchildren/autohooks.mjs export default async function (app, opts) { app.addHook('onRequest', async (req, reply) => { req.hookTwo = yes }) } ``` ```bash # app.js { autoHooks: true } $ curl http://localhost:3000/standard-plugin/ {} # no hooks in this folder, so behavior is unchanged $ curl http://localhost:3000/hooked-plugin/ { hookOne: 'yes' } $ curl http://localhost:3000/hooked-plugin/children/old {} $ curl http://localhost:3000/hooked-plugin/children/new {} $ curl http://localhost:3000/hooked-plugin/children/grandchildren/ { hookTwo: 'yes' } ``` ```bash # app.js { autoHooks: true, cascadeHooks: true } $ curl http://localhost:3000/hooked-plugin/ { hookOne: 'yes' } $ curl http://localhost:3000/hooked-plugin/children/old { hookOne: 'yes' } $ curl http://localhost:3000/hooked-plugin/children/new { hookOne: 'yes' } $ curl http://localhost:3000/hooked-plugin/children/grandchildren/ { hookOne: 'yes', hookTwo: 'yes' } # hooks are accumulated and applied in ascending order ``` ```bash # app.js { autoHooks: true, cascadeHooks: true, overwriteHooks: true } $ curl http://localhost:3000/hooked-plugin/ { hookOne: 'yes' } $ curl http://localhost:3000/hooked-plugin/children/old { hookOne: 'yes' } $ curl http://localhost:3000/hooked-plugin/children/new { hookOne: 'yes' } $ curl http://localhost:3000/hooked-plugin/children/grandchildren/ { hookTwo: 'yes' } # new autohooks.js takes over ``` ## License Licensed under [MIT](./LICENSE).