@fastify/autoload
Advanced tools
Comparing version 6.0.0-pre.fv5.1 to 6.0.0
434
index.js
'use strict' | ||
const { promises: { readdir, readFile } } = require('node:fs') | ||
const { join, relative, sep } = require('node:path') | ||
const { readFile } = require('node:fs/promises') | ||
const { join, sep } = require('node:path') | ||
const findPlugins = require('./lib/find-plugins') | ||
const runtime = require('./lib/runtime') | ||
const { pathToFileURL } = require('node:url') | ||
const runtime = require('./runtime') | ||
const routeParamPattern = /\/_/gu | ||
const routeMixedParamPattern = /__/gu | ||
const defaults = { | ||
@@ -22,233 +20,43 @@ scriptPattern: /(?:(?:^.?|\.[^d]|[^.]d|[^.][^d])\.ts|\.js|\.cjs|\.mjs|\.cts|\.mts)$/iu, | ||
const opts = { ...defaults, packageType, ...options } | ||
const pluginTree = await findPlugins(opts.dir, opts) | ||
const pluginsMeta = {} | ||
const hooksMeta = {} | ||
const pluginTree = await findPlugins(opts.dir, { opts }) | ||
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, directoryPrefix: prefix, options: opts, log: fastify.log }) | ||
.then((plugin) => { | ||
if (plugin) { | ||
// create route parameters from prefixed folders | ||
if (options.routeParams) { | ||
plugin.options.prefix = plugin.options.prefix | ||
? replaceRouteParamPattern(plugin.options.prefix) | ||
: plugin.options.prefix | ||
} | ||
pluginsMeta[plugin.name] = plugin | ||
} | ||
}) | ||
.catch((err) => { | ||
throw enrichError(err) | ||
}) | ||
})) | ||
function replaceRouteParamPattern (pattern) { | ||
const isRegularRouteParam = pattern.match(routeParamPattern) | ||
const isMixedRouteParam = pattern.match(routeMixedParamPattern) | ||
if (isMixedRouteParam) { | ||
return pattern.replace(routeMixedParamPattern, ':') | ||
} else if (isRegularRouteParam) { | ||
return pattern.replace(routeParamPattern, '/:') | ||
} else { | ||
return pattern | ||
} | ||
} | ||
await Promise.all(hookArray.map((h) => { | ||
return loadHook(h, opts) | ||
.then((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 | ||
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) | ||
} | ||
} | ||
await loadPlugins({ pluginTree, options, opts, fastify }) | ||
} | ||
async function getPackageType (cwd) { | ||
const directories = cwd.split(sep) | ||
/* c8 ignore start */ | ||
// required for paths that begin with the sep, such as linux root | ||
// ignore because OS specific evaluation | ||
directories[0] = directories[0] !== '' ? directories[0] : sep | ||
/* c8 ignore stop */ | ||
while (directories.length > 0) { | ||
const filePath = join(...directories, 'package.json') | ||
const fileContents = await readFile(filePath, 'utf-8') | ||
.catch(() => null) | ||
if (fileContents) { | ||
return JSON.parse(fileContents).type | ||
async function loadPlugins ({ pluginTree, options, opts, fastify }) { | ||
for (const key in pluginTree) { | ||
const node = { | ||
...pluginTree[key], | ||
pluginsMeta: {}, | ||
hooksMeta: {} | ||
} | ||
directories.pop() | ||
} | ||
} | ||
await Promise.all(node.plugins.map(({ file, type, prefix }) => { | ||
return loadPlugin({ file, type, directoryPrefix: prefix, options: opts, log: fastify.log }) | ||
.then((plugin) => { | ||
if (plugin) { | ||
// create route parameters from prefixed folders | ||
if (options.routeParams && plugin.options.prefix) { | ||
plugin.options.prefix = replaceRouteParamPattern(plugin.options.prefix) | ||
} | ||
node.pluginsMeta[plugin.name] = plugin | ||
} | ||
}) | ||
.catch((err) => { | ||
throw enrichError(err) | ||
}) | ||
})) | ||
const typescriptPattern = /\.(ts|mts|cts)$/iu | ||
const modulePattern = /\.(mjs|mts)$/iu | ||
const commonjsPattern = /\.(cjs|cts)$/iu | ||
function getScriptType (fname, packageType) { | ||
return { | ||
language: typescriptPattern.test(fname) ? 'typescript' : 'javascript', | ||
type: (modulePattern.test(fname) ? 'module' : commonjsPattern.test(fname) ? 'commonjs' : packageType) || 'commonjs' | ||
} | ||
} | ||
await Promise.all(node.hooks.map((h) => { | ||
return loadHook(h, opts) | ||
.then((hookPlugin) => { | ||
node.hooksMeta[h.file] = hookPlugin | ||
}) | ||
.catch((err) => { | ||
throw enrichError(err) | ||
}) | ||
})) | ||
// eslint-disable-next-line default-param-last | ||
async function findPlugins (dir, options, hookedAccumulator = {}, prefix, depth = 0, hooks = []) { | ||
const { indexPattern, ignorePattern, ignoreFilter, matchFilter, 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 = join(dir, autoHooks.name) | ||
const { type: 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 | ||
registerNode(node, fastify) | ||
} | ||
// Contains index file? | ||
const indexDirent = list.find((dirent) => indexPattern.test(dirent.name)) | ||
if (indexDirent) { | ||
const file = join(dir, indexDirent.name) | ||
const { language, type } = getScriptType(file, options.packageType) | ||
if (language === 'typescript' && !runtime.supportTypeScript) { | ||
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.`) | ||
} | ||
accumulatePlugin({ file, type }) | ||
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 && !indexDirent) { | ||
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 = 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 { language, type } = getScriptType(file, options.packageType) | ||
if (language === 'typescript' && !runtime.supportTypeScript) { | ||
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.`) | ||
} | ||
// Don't place hook in plugin queue | ||
if (!autoHooksPattern.test(dirent.name)) { | ||
accumulatePlugin({ file, type }) | ||
} | ||
} | ||
} | ||
await Promise.all(directoryPromises) | ||
return hookedAccumulator | ||
function accumulatePlugin ({ file, type }) { | ||
// Replace backward slash to forward slash for consistent behavior between windows and posix. | ||
const filePath = '/' + relative(options.dir, file).replace(/\\/gu, '/') | ||
if (matchFilter && !filterPath(filePath, matchFilter)) { | ||
return | ||
} | ||
if (ignoreFilter && filterPath(filePath, ignoreFilter)) { | ||
return | ||
} | ||
hookedAccumulator[prefix || '/'].plugins.push({ file, type, prefix }) | ||
} | ||
} | ||
@@ -274,14 +82,3 @@ | ||
const plugin = wrapRoutes(content.default || content) | ||
const pluginConfig = (content.default && content.default.autoConfig) || content.autoConfig || {} | ||
let pluginOptions | ||
if (typeof pluginConfig === 'function') { | ||
pluginOptions = function (fastify) { | ||
return { ...pluginConfig(fastify), ...overrideConfig } | ||
} | ||
pluginOptions.prefix = overrideConfig.prefix ?? pluginConfig.prefix | ||
} else { | ||
pluginOptions = { ...pluginConfig, ...overrideConfig } | ||
} | ||
const pluginOptions = loadPluginOptions(content, overrideConfig) | ||
const pluginMeta = plugin[Symbol.for('plugin-meta')] || {} | ||
@@ -303,10 +100,3 @@ | ||
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(/\/+/gu, '/') | ||
} | ||
handlePrefixConfig({ plugin, pluginOptions, content, directoryPrefix }) | ||
@@ -323,2 +113,48 @@ return { | ||
async function loadHook (hook, options) { | ||
let hookContent | ||
if (options.forceESM || hook.type === 'module' || runtime.forceESM) { | ||
hookContent = await import(pathToFileURL(hook.file).href) | ||
} else { | ||
hookContent = require(hook.file) | ||
} | ||
hookContent = hookContent.default || hookContent | ||
const type = Object.prototype.toString.call(hookContent) | ||
if (type === '[object AsyncFunction]' || type === '[object Function]') { | ||
hookContent[Symbol.for('skip-override')] = true | ||
} | ||
return hookContent | ||
} | ||
function registerNode (node, fastify) { | ||
if (node.hooks.length === 0) { | ||
registerAllPlugins(fastify, node) | ||
} else { | ||
const composedPlugin = async function (app) { | ||
// find hook functions for this prefix | ||
for (const hookFile of node.hooks) { | ||
const hookPlugin = node.hooksMeta[hookFile.file] | ||
// encapsulate hooks at plugin level | ||
app.register(hookPlugin) | ||
} | ||
registerAllPlugins(app, node) | ||
} | ||
fastify.register(composedPlugin) | ||
} | ||
} | ||
function registerAllPlugins (app, node) { | ||
const metas = Object.values(node.pluginsMeta) | ||
for (const pluginFile of node.plugins) { | ||
// 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, node.pluginsMeta) | ||
} | ||
} | ||
function registerPlugin (fastify, meta, allPlugins, parentPlugins = {}) { | ||
@@ -349,14 +185,48 @@ const { plugin, name, options, dependencies = [] } = meta | ||
function filterPath (path, filter) { | ||
if (typeof filter === 'string') { | ||
return path.includes(filter) | ||
function loadPluginOptions (content, overrideConfig) { | ||
const pluginConfig = (content.default?.autoConfig) || content.autoConfig || {} | ||
if (typeof pluginConfig === 'function') { | ||
const pluginOptions = (fastify) => ({ ...pluginConfig(fastify), ...overrideConfig }) | ||
pluginOptions.prefix = overrideConfig.prefix ?? pluginConfig.prefix | ||
return pluginOptions | ||
} | ||
if (filter instanceof RegExp) { | ||
return filter.test(path) | ||
return { ...pluginConfig, ...overrideConfig } | ||
} | ||
function handlePrefixConfig ({ plugin, pluginOptions, content, directoryPrefix }) { | ||
if (pluginOptions.prefix?.endsWith('/')) { | ||
pluginOptions.prefix = pluginOptions.prefix.slice(0, -1) | ||
} | ||
return filter(path) | ||
let prefix | ||
if (plugin.autoPrefix !== undefined) { | ||
prefix = plugin.autoPrefix | ||
} else if (content.autoPrefix !== undefined) { | ||
prefix = content.autoPrefix | ||
} else { | ||
prefix = directoryPrefix | ||
} | ||
const prefixOverride = plugin.prefixOverride ?? content.prefixOverride | ||
if (prefixOverride !== undefined) { | ||
pluginOptions.prefix = prefixOverride | ||
} else if (prefix) { | ||
pluginOptions.prefix = (pluginOptions.prefix || '') + prefix.replace(/\/+/gu, '/') | ||
} | ||
} | ||
const routeParamPattern = /\/_/gu | ||
const routeMixedParamPattern = /__/gu | ||
function replaceRouteParamPattern (pattern) { | ||
if (pattern.match(routeMixedParamPattern)) { | ||
return pattern.replace(routeMixedParamPattern, ':') | ||
} else if (pattern.match(routeParamPattern)) { | ||
return pattern.replace(routeParamPattern, '/:') | ||
} | ||
return pattern | ||
} | ||
/** | ||
@@ -372,8 +242,5 @@ * Used to determine if the contents of a required autoloaded file matches | ||
function isRouteObject (input) { | ||
if (input && | ||
Object.prototype.toString.call(input) === '[object Object]' && | ||
Object.prototype.hasOwnProperty.call(input, 'method')) { | ||
return true | ||
} | ||
return false | ||
return !!(input && | ||
Object.prototype.toString.call(input) === '[object Object]' && | ||
Object.hasOwn(input, 'method')) | ||
} | ||
@@ -398,3 +265,3 @@ | ||
result = true | ||
} else if (Object.prototype.hasOwnProperty.call(input, 'default')) { | ||
} else if (Object.hasOwn(input, 'default')) { | ||
result = isPluginOrModule(input.default) | ||
@@ -414,25 +281,6 @@ } else { | ||
} | ||
return content | ||
} | ||
async function loadHook (hook, options) { | ||
let hookContent | ||
if (options.forceESM || hook.type === 'module' || runtime.forceESM) { | ||
hookContent = await import(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) { | ||
@@ -449,2 +297,24 @@ // Hack SyntaxError message so that we provide | ||
async function getPackageType (cwd) { | ||
const directories = cwd.split(sep) | ||
/* c8 ignore start */ | ||
// required for paths that begin with the sep, such as linux root | ||
// ignore because OS specific evaluation | ||
directories[0] = directories[0] !== '' ? directories[0] : sep | ||
/* c8 ignore stop */ | ||
while (directories.length > 0) { | ||
const filePath = join(...directories, 'package.json') | ||
const fileContents = await readFile(filePath, 'utf-8') | ||
.catch(() => null) | ||
if (fileContents) { | ||
return JSON.parse(fileContents).type | ||
} | ||
directories.pop() | ||
} | ||
} | ||
// do not create a new context, do not encapsulate | ||
@@ -451,0 +321,0 @@ // same as fastify-plugin |
{ | ||
"name": "@fastify/autoload", | ||
"version": "6.0.0-pre.fv5.1", | ||
"version": "6.0.0", | ||
"description": "Require all plugins in a directory", | ||
@@ -47,12 +47,12 @@ "main": "index.js", | ||
"@fastify/pre-commit": "^2.1.0", | ||
"@fastify/url-data": "^6.0.0-pre.fv5.1", | ||
"@fastify/url-data": "^6.0.0", | ||
"@swc-node/register": "^1.9.1", | ||
"@swc/core": "^1.5.25", | ||
"@types/jest": "^29.5.12", | ||
"@types/node": "^20.14.2", | ||
"@types/node": "^22.0.0", | ||
"@types/tap": "^15.0.11", | ||
"esbuild": "^0.21.4", | ||
"esbuild": "^0.23.0", | ||
"esbuild-register": "^3.5.0", | ||
"fastify": "^5.0.0-alpha.3", | ||
"fastify-plugin": "^5.0.0-pre.fv5.1", | ||
"fastify": "^5.0.0-alpha.4", | ||
"fastify-plugin": "^5.0.0", | ||
"jest": "^29.7.0", | ||
@@ -70,3 +70,3 @@ "snazzy": "^9.0.0", | ||
"vite": "^5.2.12", | ||
"vitest": "^1.6.0" | ||
"vitest": "^2.0.3" | ||
}, | ||
@@ -73,0 +73,0 @@ "standard": { |
@@ -152,3 +152,3 @@ # @fastify/autoload | ||
}) | ||
``` | ||
``` | ||
@@ -155,0 +155,0 @@ - `indexPattern` (optional) - Regex to override the `index.js` naming convention |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
138170
267
4172
0