UNPKG

fastify-autoload

Version:
331 lines (280 loc) 11.6 kB
'use strict' const path = require('path') const url = require('url') const { readdir } = require('fs').promises const pkgUp = require('pkg-up') const semver = require('semver') const isTsNode = (Symbol.for('ts-node.register.instance') in process) || !!process.env.TS_NODE_DEV const isJestEnviroment = process.env.JEST_WORKER_ID !== undefined const typescriptSupport = isTsNode || isJestEnviroment const moduleSupport = semver.satisfies(process.version, '>= 14 || >= 12.17.0 < 13.0.0') const defaults = { scriptPattern: /((^.?|\.[^d]|[^.]d|[^.][^d])\.ts|\.js|\.cjs|\.mjs)$/i, indexPattern: /^index(\.ts|\.js|\.cjs|\.mjs)$/i, autoHooksPattern: /^[_.]?auto_?hooks(\.ts|\.js|\.cjs|\.mjs)$/i, dirNameRoutePrefix: true } const fastifyAutoload = async function autoload (fastify, options) { const packageType = await getPackageType(options.dir) const opts = { ...defaults, packageType, ...options } const pluginTree = await findPlugins(opts.dir, opts) const pluginsMeta = {} const hooksMeta = {} const pluginArray = [].concat.apply([], Object.values(pluginTree).map(o => o.plugins)) const hookArray = [].concat.apply([], Object.values(pluginTree).map(o => o.hooks)) await Promise.all(pluginArray.map(({ file, type, prefix }) => { return loadPlugin(file, type, prefix, opts) .then((plugin) => { if (plugin) { pluginsMeta[plugin.name] = plugin } }) .catch((err) => { throw enrichError(err) }) })) await Promise.all(hookArray.map((h) => { if (hooksMeta[h.file]) return null // hook plugin already loaded, skip this instance return loadHook(h, opts) .then((hookPlugin) => { if (hookPlugin) { hooksMeta[h.file] = hookPlugin } }) .catch((err) => { throw enrichError(err) }) })) const metas = Object.values(pluginsMeta) for (const prefix in pluginTree) { const hookFiles = pluginTree[prefix].hooks const pluginFiles = pluginTree[prefix].plugins if (hookFiles.length === 0) { registerAllPlugins(fastify, pluginFiles) } else { const composedPlugin = async function (app) { // find hook functions for this prefix for (const hookFile of hookFiles) { const hookPlugin = hooksMeta[hookFile.file] // encapsulate hooks at plugin level if (hookPlugin) app.register(hookPlugin) } registerAllPlugins(app, pluginFiles) } fastify.register(composedPlugin) } } function registerAllPlugins (app, pluginFiles) { for (const pluginFile of pluginFiles) { // find plugins for this prefix, based on filename stored in registerPlugins() const plugin = metas.find((i) => i.filename === pluginFile.file) // register plugins at fastify level if (plugin) registerPlugin(app, plugin, pluginsMeta) } } } async function getPackageType (cwd) { const nearestPackage = await pkgUp({ cwd }) if (nearestPackage) { return require(nearestPackage).type } } const typescriptPattern = /\.ts$/i const modulePattern = /\.mjs$/i const commonjsPattern = /\.cjs$/i function getScriptType (fname, packageType) { return (modulePattern.test(fname) ? 'module' : commonjsPattern.test(fname) ? 'commonjs' : typescriptPattern.test(fname) ? 'typescript' : packageType) || 'commonjs' } // eslint-disable-next-line default-param-last async function findPlugins (dir, options, hookedAccumulator = {}, prefix, depth = 0, hooks = []) { const { indexPattern, ignorePattern, scriptPattern, dirNameRoutePrefix, maxDepth, autoHooksPattern } = options const list = await readdir(dir, { withFileTypes: true }) let currentHooks = [] // check to see if hooks or plugins have been added to this prefix, initialize if not if (!hookedAccumulator[prefix || '/']) hookedAccumulator[prefix || '/'] = { hooks: [], plugins: [] } if (options.autoHooks) { // Hooks were passed in, create new array specific to this plugin item if (hooks && hooks.length > 0) { for (const hook of hooks) { currentHooks.push(hook) } } // Contains autohooks file? const autoHooks = list.find((dirent) => autoHooksPattern.test(dirent.name)) if (autoHooks) { const autoHooksFile = path.join(dir, autoHooks.name) const autoHooksType = getScriptType(autoHooksFile, options.packageType) // Overwrite current hooks? if (options.overwriteHooks && currentHooks.length > 0) { currentHooks = [] } // Add hook to current chain currentHooks.push({ file: autoHooksFile, type: autoHooksType }) } hookedAccumulator[prefix || '/'].hooks = currentHooks } // Contains index file? const indexDirent = list.find((dirent) => indexPattern.test(dirent.name)) if (indexDirent) { const file = path.join(dir, indexDirent.name) const type = getScriptType(file, options.packageType) if (type === 'typescript' && !typescriptSupport) { throw new Error(`fastify-autoload cannot import hooks plugin at '${file}'. To fix this error compile TypeScript to JavaScript or use 'ts-node' to run your app.`) } if (type === 'module' && !moduleSupport) { throw new Error(`fastify-autoload cannot import hooks plugin at '${file}'. Your version of node does not support ES modules. To fix this error upgrade to Node 14 or use CommonJS syntax.`) } hookedAccumulator[prefix || '/'].plugins.push({ file, type, prefix }) const hasDirectory = list.find((dirent) => dirent.isDirectory()) if (!hasDirectory) { return hookedAccumulator } } // Contains package.json but no index.js file? const packageDirent = list.find((dirent) => dirent.name === 'package.json') if (packageDirent) { throw new Error(`fastify-autoload cannot import plugin at '${dir}'. To fix this error rename the main entry file to 'index.js' (or .cjs, .mjs, .ts).`) } // Otherwise treat each script file as a plugin const directoryPromises = [] for (const dirent of list) { if (ignorePattern && dirent.name.match(ignorePattern)) { continue } const atMaxDepth = Number.isFinite(maxDepth) && maxDepth <= depth const file = path.join(dir, dirent.name) if (dirent.isDirectory() && !atMaxDepth) { let prefixBreadCrumb = (prefix ? `${prefix}/` : '/') if (dirNameRoutePrefix === true) { prefixBreadCrumb += dirent.name } else if (typeof dirNameRoutePrefix === 'function') { const prefixReplacer = dirNameRoutePrefix(dir, dirent.name) if (prefixReplacer) { prefixBreadCrumb += prefixReplacer } } // Pass hooks forward to next level if (options.autoHooks && options.cascadeHooks) { directoryPromises.push(findPlugins(file, options, hookedAccumulator, prefixBreadCrumb, depth + 1, currentHooks)) } else { directoryPromises.push(findPlugins(file, options, hookedAccumulator, prefixBreadCrumb, depth + 1)) } continue } else if (indexDirent) { // An index.js file is present in the directory so we ignore the others modules (but not the subdirectories) continue } if (dirent.isFile() && scriptPattern.test(dirent.name)) { const type = getScriptType(file, options.packageType) if (type === 'typescript' && !typescriptSupport) { throw new Error(`fastify-autoload cannot import plugin at '${file}'. To fix this error compile TypeScript to JavaScript or use 'ts-node' to run your app.`) } if (type === 'module' && !moduleSupport) { throw new Error(`fastify-autoload cannot import plugin at '${file}'. Your version of node does not support ES modules. To fix this error upgrade to Node 14 or use CommonJS syntax.`) } // Don't place hook in plugin queue if (!autoHooksPattern.test(dirent.name)) { hookedAccumulator[prefix || '/'].plugins.push({ file, type, prefix }) } } } await Promise.all(directoryPromises) return hookedAccumulator } async function loadPlugin (file, type, directoryPrefix, options) { const { options: overrideConfig, forceESM } = options let content if (forceESM || type === 'module') { content = await import(url.pathToFileURL(file).href) } else { content = require(file) } const plugin = wrapRoutes(content.default || content) const pluginConfig = (content.default && content.default.autoConfig) || content.autoConfig || {} const pluginOptions = Object.assign({}, pluginConfig, overrideConfig) const pluginMeta = plugin[Symbol.for('plugin-meta')] || {} if (plugin.autoload === false || content.autoload === false) { return } // Reset to support overriding autoConfig for library plugins if (plugin.autoConfig !== undefined) { plugin.autoConfig = undefined } pluginOptions.prefix = (pluginOptions.prefix && pluginOptions.prefix.endsWith('/')) ? pluginOptions.prefix.slice(0, -1) : pluginOptions.prefix const prefixOverride = plugin.prefixOverride !== undefined ? plugin.prefixOverride : content.prefixOverride !== undefined ? content.prefixOverride : undefined const prefix = (plugin.autoPrefix !== undefined ? plugin.autoPrefix : content.autoPrefix !== undefined ? content.autoPrefix : undefined) || directoryPrefix if (prefixOverride !== undefined) { pluginOptions.prefix = prefixOverride } else if (prefix) { pluginOptions.prefix = (pluginOptions.prefix || '') + prefix.replace(/\/+/g, '/') } return { plugin, filename: file, name: pluginMeta.name || file, dependencies: pluginMeta.dependencies, options: pluginOptions, registered: false } } function registerPlugin (fastify, meta, allPlugins, parentPlugins = {}) { const { plugin, name, options, dependencies = [] } = meta if (parentPlugins[name]) { throw new Error('Cyclic dependency') } if (meta.registered) { return } parentPlugins[name] = true for (const name of dependencies) { if (allPlugins[name]) { // we create a shallow copy of parentPlugins so we can load once the ones that are // on two different dependency chains registerPlugin(fastify, allPlugins[name], allPlugins, { ...parentPlugins }) } } fastify.register(plugin, options) meta.registered = true } function wrapRoutes (content) { if (content && Object.prototype.toString.call(content) === '[object Object]' && Object.prototype.hasOwnProperty.call(content, 'method')) { return async function (fastify, opts) { fastify.route(content) } } return content } async function loadHook (hook, options) { if (!hook) return null let hookContent if (options.forceESM || hook.type === 'module') { hookContent = await import(url.pathToFileURL(hook.file).href) } else { hookContent = require(hook.file) } hookContent = hookContent.default || hookContent if ( Object.prototype.toString.call(hookContent) === '[object AsyncFunction]' || Object.prototype.toString.call(hookContent) === '[object Function]' ) { hookContent[Symbol.for('skip-override')] = true } return hookContent } function enrichError (err) { // Hack SyntaxError message so that we provide // the line number to the user, otherwise they // will be left in the cold. if (err instanceof SyntaxError) { err.message += ' at ' + err.stack.split('\n')[0] } return err } // do not create a new context, do not encapsulate // same as fastify-plugin fastifyAutoload[Symbol.for('skip-override')] = true module.exports = fastifyAutoload module.exports.fastifyAutoload = fastifyAutoload module.exports.default = fastifyAutoload