Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 18 additions & 40 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,71 +1,49 @@
'use strict'

const symbols = require('./lib/symbols')
const prepareMetadata = require('./lib/prepareMetadata')
const getPluginName = require('./lib/getPluginName')
const toCamelCase = require('./lib/toCamelCase')

const kSkipOverride = Symbol.for('skip-override')
const kDisplayName = Symbol.for('fastify.display-name')
const kPluginMeta = Symbol.for('plugin-meta')

let count = 0

function plugin (fn, options = {}) {
let autoName = false
/**
* @param { import('./types/index').FastifyPluginFunction } fn
* @param { import('./types/index').PluginMetadata | string } metadata
* @returns { import('./types/index').FastifyPluginFunction }
*/
function plugin (fn, metadata = {}) {
let pluginMetadata = prepareMetadata(metadata)

if (fn?.default !== undefined) {
// Support for 'export default' behaviour in transpiled ECMAScript module
fn = fn.default
}

if (typeof fn !== 'function') {
throw new TypeError(
`fastify-plugin expects a function, instead got a '${typeof fn}'`
)
throw new TypeError(`fastify-plugin expects a function, instead got a '${typeof fn}'`)
}

if (typeof options === 'string') {
options = {
fastify: options
}
}

if (
typeof options !== 'object' ||
Array.isArray(options) ||
options === null
) {
throw new TypeError('The options object should be an object')
}

if (options.fastify !== undefined && typeof options.fastify !== 'string') {
throw new TypeError(`fastify-plugin expects a version string, instead got '${typeof options.fastify}'`)
}

if (!options.name) {
let autoName = false
if (!pluginMetadata.name) {
autoName = true
options = { ...options, name: getPluginName(fn) + '-auto-' + count++ }
pluginMetadata = { ...pluginMetadata, name: getPluginName(fn) + '-auto-' + count++ }
}

fn[kSkipOverride] = options.encapsulate !== true
fn[kDisplayName] = options.name
fn[kPluginMeta] = options
fn[symbols.kSkipOverride] = pluginMetadata.encapsulate !== true
fn[symbols.kDisplayName] = pluginMetadata.name
fn[symbols.kPluginMeta] = pluginMetadata

// Faux modules support
if (!fn.default) {
fn.default = fn
}

// TypeScript support for named imports
// See https://github.com/fastify/fastify/issues/2404 for more details
// The type definitions would have to be update to match this.
const camelCase = toCamelCase(options.name)
const camelCase = toCamelCase(pluginMetadata.name)
if (!autoName && !fn[camelCase]) {
fn[camelCase] = fn
}

return fn
}

module.exports = plugin
module.exports.default = plugin
module.exports.fastifyPlugin = plugin
module.exports.symbols = symbols
33 changes: 23 additions & 10 deletions lib/getPluginName.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
'use strict'

const FP_STACK_TRACE_REG = /at\s(?:.*\.)?plugin\s.*\n\s*(.*)/
const FILE_NAME_REG = /(\w*(\.\w*)*)\..*/

module.exports = function getPluginName (fn) {
if (fn.name.length > 0) return fn.name
const STACK_TRACE = /at\s(?:.*\.)?plugin\s.*\n\s*(.*)/
const FILE_NAME = /(\w*(\.\w*)*)\..*/

/**
* @param { import('../types/index').FastifyPluginFunction } fn
* @returns { string }
*/
function getPluginName (fn) {
if (fn.name.length > 0) {
return fn.name
}
const stackTraceLimit = Error.stackTraceLimit
Error.stackTraceLimit = 10
try {
Expand All @@ -16,10 +21,18 @@ module.exports = function getPluginName (fn) {
}
}

function extractPluginName (stack) {
const m = stack.match(FP_STACK_TRACE_REG)

// get last section of path and match for filename
return m ? m[1].split(/[/\\]/).slice(-1)[0].match(FILE_NAME_REG)[1] : 'anonymous'
/**
* @param { string } stackTrace
* @returns { string }
*/
function extractPluginName (stackTrace) {
const match = stackTrace.match(STACK_TRACE)
if (!match) {
return 'anonymous'
}
const fileName = match[1].split(/[/\\]/).pop()
return fileName.match(FILE_NAME)[1]
}

module.exports = getPluginName
module.exports.extractPluginName = extractPluginName
27 changes: 27 additions & 0 deletions lib/prepareMetadata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use strict'

/**
* @param { import('../types/index').PluginMetadata | string } metadata
* @returns { import('../types/index').PluginMetadata }
*/
function prepareMetadata (metadata) {
const normalizedMetadata = typeof metadata === 'string' ? { fastify: metadata } : metadata
const isInvalidMetadata = (
normalizedMetadata === null ||
typeof normalizedMetadata !== 'object' ||
Array.isArray(normalizedMetadata)
)
if (isInvalidMetadata) {
const actualType = metadata === null
? 'null'
: (Array.isArray(metadata) ? 'array' : typeof metadata)
throw new TypeError(`The metadata should be an object or version string, not '${actualType}'`)
}
const fastifyVersion = normalizedMetadata.fastify
if (fastifyVersion !== undefined && typeof fastifyVersion !== 'string') {
throw new TypeError(`fastify-plugin expects a version string, instead got '${typeof fastifyVersion}'`)
}
return normalizedMetadata
}

module.exports = prepareMetadata
7 changes: 7 additions & 0 deletions lib/symbols.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict'

module.exports = {
kSkipOverride: Symbol.for('skip-override'),
kDisplayName: Symbol.for('fastify.display-name'),
kPluginMeta: Symbol.for('plugin-meta')
}
20 changes: 15 additions & 5 deletions lib/toCamelCase.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@

const KEBAB_REG = /-(.)/g

function upperFirst (_match, char) {
return char.toUpperCase()
}

module.exports = function toCamelCase (name) {
/**
* @param { string } name
* @returns { string }
*/
function toCamelCase (name) {
if (name[0] === '@') {
name = name.slice(1).replace('/', '-')
}
return name.replace(KEBAB_REG, upperFirst)
}

/**
* @param { string } char
* @returns { string }
*/
function upperFirst (_, char) {
return char.toUpperCase()
}

module.exports = toCamelCase
5 changes: 3 additions & 2 deletions test/composite.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

const { test } = require('node:test')
const { kPluginMeta, kDisplayName } = require('../lib/symbols')
const fp = require('..')

test('anonymous function should be named composite.test0', (t) => {
Expand All @@ -9,6 +10,6 @@ test('anonymous function should be named composite.test0', (t) => {
next()
})

t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'composite.test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'composite.test-auto-0')
t.assert.strictEqual(fn[kPluginMeta].name, 'composite.test-auto-0')
t.assert.strictEqual(fn[kDisplayName], 'composite.test-auto-0')
})
5 changes: 3 additions & 2 deletions test/mu1tip1e.composite.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

const { test } = require('node:test')
const { kPluginMeta, kDisplayName } = require('../lib/symbols')
const fp = require('..')

test('anonymous function should be named mu1tip1e.composite.test', (t) => {
Expand All @@ -10,6 +11,6 @@ test('anonymous function should be named mu1tip1e.composite.test', (t) => {
next()
})

t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'mu1tip1e.composite.test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'mu1tip1e.composite.test-auto-0')
t.assert.strictEqual(fn[kPluginMeta].name, 'mu1tip1e.composite.test-auto-0')
t.assert.strictEqual(fn[kDisplayName], 'mu1tip1e.composite.test-auto-0')
})
50 changes: 29 additions & 21 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

const { test } = require('node:test')
const { kDisplayName, kPluginMeta, kSkipOverride } = require('../lib/symbols')
const proxyquire = require('proxyquire')
const fp = require('..')
const Fastify = require('fastify')
Expand All @@ -20,7 +21,7 @@ test('should return the function with the skip-override Symbol', (t) => {
}

fp(plugin)
t.assert.ok(plugin[Symbol.for('skip-override')])
t.assert.ok(plugin[kSkipOverride])
})

test('should support "default" function from babel module', (t) => {
Expand Down Expand Up @@ -93,20 +94,27 @@ test('should check the fastify version', (t) => {
})

test('the options object should be an object', (t) => {
t.plan(2)
t.plan(3)

try {
fp(() => { }, null)
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'The options object should be an object')
t.assert.strictEqual(e.message, "The metadata should be an object or version string, not 'null'")
}

try {
fp(() => { }, [])
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'The options object should be an object')
t.assert.strictEqual(e.message, "The metadata should be an object or version string, not 'array'")
}

try {
fp(() => { }, true)
t.assert.fail()
} catch (e) {
t.assert.strictEqual(e.message, 'The metadata should be an object or version string, not \'boolean\'')
}
})

Expand All @@ -132,8 +140,8 @@ test('Should accept an option object', (t) => {

fp(plugin, opts)

const meta = plugin[Symbol.for('plugin-meta')]
t.assert.ok(plugin[Symbol.for('skip-override')], 'skip-override symbol should be present')
const meta = plugin[kPluginMeta]
t.assert.ok(plugin[kSkipOverride], 'skip-override symbol should be present')
t.assert.strictEqual(meta.hello, 'world', 'plugin-meta should carry the options')
t.assert.match(meta.name, /^plugin-auto-\d+$/, 'plugin-meta should carry the generated name')
t.assert.deepStrictEqual(opts, { hello: 'world' }, 'options must not be mutated')
Expand All @@ -149,8 +157,8 @@ test('Should accept an option object and checks the version', (t) => {
}

fp(plugin, opts)
const meta = plugin[Symbol.for('plugin-meta')]
t.assert.ok(plugin[Symbol.for('skip-override')])
const meta = plugin[kPluginMeta]
t.assert.ok(plugin[kSkipOverride])
t.assert.deepStrictEqual({ hello: meta.hello, fastify: meta.fastify }, opts)
t.assert.deepStrictEqual(opts, { hello: 'world', fastify: '>=0.10.0' }, 'options must not be mutated')
})
Expand All @@ -161,7 +169,7 @@ test('Should keep options by reference when a name is given', (t) => {
const opts = { name: 'by-reference', fastify: '>=0.10.0' }
const plugin = fp((_fastify, _opts, next) => next(), opts)

t.assert.strictEqual(plugin[Symbol.for('plugin-meta')], opts)
t.assert.strictEqual(plugin[kPluginMeta], opts)
})

test('Should give distinct auto names to plugins that share one options object', (t) => {
Expand All @@ -171,8 +179,8 @@ test('Should give distinct auto names to plugins that share one options object',
const first = fp((_fastify, _opts, next) => next(), shared)
const second = fp((_fastify, _opts, next) => next(), shared)

const firstName = first[Symbol.for('plugin-meta')].name
const secondName = second[Symbol.for('plugin-meta')].name
const firstName = first[kPluginMeta].name
const secondName = second[kPluginMeta].name
t.assert.match(firstName, /^test-auto-\d+$/)
t.assert.match(secondName, /^test-auto-\d+$/)
t.assert.notStrictEqual(firstName, secondName, 'the second plugin must not inherit the first auto name')
Expand All @@ -186,15 +194,15 @@ test('should set anonymous function name to file it was called from with a count
next()
})

t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'test-auto-0')
t.assert.strictEqual(fn[kPluginMeta].name, 'test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'test-auto-0')

const fn2 = fp((_fastify, _opts, next) => {
next()
})

t.assert.strictEqual(fn2[Symbol.for('plugin-meta')].name, 'test-auto-1')
t.assert.strictEqual(fn2[Symbol.for('fastify.display-name')], 'test-auto-1')
t.assert.strictEqual(fn2[kPluginMeta].name, 'test-auto-1')
t.assert.strictEqual(fn2[kDisplayName], 'test-auto-1')
})

test('should set function name if Error.stackTraceLimit is set to 0', (t) => {
Expand All @@ -206,15 +214,15 @@ test('should set function name if Error.stackTraceLimit is set to 0', (t) => {
next()
})

t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, 'test-auto-0')
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], 'test-auto-0')
t.assert.strictEqual(fn[kPluginMeta].name, 'test-auto-0')
t.assert.strictEqual(fn[kDisplayName], 'test-auto-0')

const fn2 = fp((_fastify, _opts, next) => {
next()
})

t.assert.strictEqual(fn2[Symbol.for('plugin-meta')].name, 'test-auto-1')
t.assert.strictEqual(fn2[Symbol.for('fastify.display-name')], 'test-auto-1')
t.assert.strictEqual(fn2[kPluginMeta].name, 'test-auto-1')
t.assert.strictEqual(fn2[kDisplayName], 'test-auto-1')

Error.stackTraceLimit = stackTraceLimit
})
Expand All @@ -228,8 +236,8 @@ test('should set display-name to meta name', (t) => {
name: functionName
})

t.assert.strictEqual(fn[Symbol.for('plugin-meta')].name, functionName)
t.assert.strictEqual(fn[Symbol.for('fastify.display-name')], functionName)
t.assert.strictEqual(fn[kPluginMeta].name, functionName)
t.assert.strictEqual(fn[kDisplayName], functionName)
})

test('should preserve fastify version in meta', (t) => {
Expand All @@ -239,7 +247,7 @@ test('should preserve fastify version in meta', (t) => {

const fn = fp((_fastify, _opts, next) => next(), opts)

t.assert.strictEqual(fn[Symbol.for('plugin-meta')].fastify, '>=0.10.0')
t.assert.strictEqual(fn[kPluginMeta].fastify, '>=0.10.0')
})

test('should check fastify dependency graph - plugin', async (t) => {
Expand Down
9 changes: 9 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ declare namespace fastifyPlugin {
encapsulate?: boolean
}

export type FastifyPluginFunction<
Options extends FastifyPluginOptions = Record<never, never>,
RawServer extends RawServerBase = RawServerDefault,
TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault,
Logger extends FastifyBaseLogger = FastifyBaseLogger
> =
| FastifyPluginCallback<Options, RawServer, TypeProvider, Logger>
| FastifyPluginAsync<Options, RawServer, TypeProvider, Logger>

export const fastifyPlugin: FastifyPlugin
export { fastifyPlugin as default }
}
Expand Down