From dbf314d124c14457011de8be7b458be7fdddc01f Mon Sep 17 00:00:00 2001 From: nhankyjangchan Date: Fri, 11 Sep 2026 15:02:23 +0300 Subject: [PATCH] refactor(lib): organizational changes to the package structure - extracting symbols into lib/symbols.js - refactoring to improve the readability of lib/getPluginName.js - extracting metadata normalization and validation logic into lib/prepareMetadata.js - adding JSDoc annotations above package functions - added FastifyPluginFunction type - added new section of test to verify correct metadata normalization Signed-off-by: nhankyjangchan --- index.js | 58 ++++++++++----------------------- lib/getPluginName.js | 33 +++++++++++++------ lib/prepareMetadata.js | 27 +++++++++++++++ lib/symbols.js | 7 ++++ lib/toCamelCase.js | 20 +++++++++--- test/composite.test.js | 5 +-- test/mu1tip1e.composite.test.js | 5 +-- test/test.js | 50 ++++++++++++++++------------ types/index.d.ts | 9 +++++ 9 files changed, 134 insertions(+), 80 deletions(-) create mode 100644 lib/prepareMetadata.js create mode 100644 lib/symbols.js diff --git a/index.js b/index.js index e466fa2..df1c451 100644 --- a/index.js +++ b/index.js @@ -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 diff --git a/lib/getPluginName.js b/lib/getPluginName.js index 70e9711..5b65e58 100644 --- a/lib/getPluginName.js +++ b/lib/getPluginName.js @@ -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 { @@ -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 diff --git a/lib/prepareMetadata.js b/lib/prepareMetadata.js new file mode 100644 index 0000000..41546a5 --- /dev/null +++ b/lib/prepareMetadata.js @@ -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 diff --git a/lib/symbols.js b/lib/symbols.js new file mode 100644 index 0000000..1f8d74c --- /dev/null +++ b/lib/symbols.js @@ -0,0 +1,7 @@ +'use strict' + +module.exports = { + kSkipOverride: Symbol.for('skip-override'), + kDisplayName: Symbol.for('fastify.display-name'), + kPluginMeta: Symbol.for('plugin-meta') +} diff --git a/lib/toCamelCase.js b/lib/toCamelCase.js index 7439af2..cf086d4 100644 --- a/lib/toCamelCase.js +++ b/lib/toCamelCase.js @@ -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 diff --git a/test/composite.test.js b/test/composite.test.js index db71b7b..41d3ef3 100644 --- a/test/composite.test.js +++ b/test/composite.test.js @@ -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) => { @@ -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') }) diff --git a/test/mu1tip1e.composite.test.js b/test/mu1tip1e.composite.test.js index dddfecf..d95ec96 100644 --- a/test/mu1tip1e.composite.test.js +++ b/test/mu1tip1e.composite.test.js @@ -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) => { @@ -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') }) diff --git a/test/test.js b/test/test.js index d92f7a6..68277f2 100644 --- a/test/test.js +++ b/test/test.js @@ -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') @@ -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) => { @@ -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\'') } }) @@ -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') @@ -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') }) @@ -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) => { @@ -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') @@ -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) => { @@ -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 }) @@ -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) => { @@ -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) => { diff --git a/types/index.d.ts b/types/index.d.ts index 11fffd5..7829d7b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -27,6 +27,15 @@ declare namespace fastifyPlugin { encapsulate?: boolean } + export type FastifyPluginFunction< + Options extends FastifyPluginOptions = Record, + RawServer extends RawServerBase = RawServerDefault, + TypeProvider extends FastifyTypeProvider = FastifyTypeProviderDefault, + Logger extends FastifyBaseLogger = FastifyBaseLogger + > = + | FastifyPluginCallback + | FastifyPluginAsync + export const fastifyPlugin: FastifyPlugin export { fastifyPlugin as default } }