diff --git a/@commitlint/load/src/utils/load-plugin.test.ts b/@commitlint/load/src/utils/load-plugin.test.ts index abe9b74bc6..0cd5c0e6b4 100644 --- a/@commitlint/load/src/utils/load-plugin.test.ts +++ b/@commitlint/load/src/utils/load-plugin.test.ts @@ -1,4 +1,6 @@ import loadPlugin from './load-plugin'; +import {platform} from 'os'; +import chalk from 'chalk'; jest.mock('commitlint-plugin-example', () => ({example: true}), { virtual: true, @@ -8,6 +10,22 @@ jest.mock('@scope/commitlint-plugin-example', () => ({scope: true}), { virtual: true, }); +jest.mock('./relative/posix.js', () => ({relativePosix: true}), { + virtual: true, +}); + +jest.mock('/absolute/posix.js', () => ({relativePosix: true}), { + virtual: true, +}); + +jest.mock('.\\relative\\windows.js', () => ({relativePosix: true}), { + virtual: true, +}); + +jest.mock('C:\\absolute\\windows.js', () => ({relativePosix: true}), { + virtual: true, +}); + test('should load a plugin when referenced by short name', () => { const plugins = loadPlugin({}, 'example'); expect(plugins['example']).toBe(require('commitlint-plugin-example')); @@ -34,9 +52,14 @@ test('should throw an error when a plugin has whitespace', () => { }); test("should throw an error when a plugin doesn't exist", () => { + const spy = jest.spyOn(console, 'error').mockImplementation(); expect(() => loadPlugin({}, 'nonexistentplugin')).toThrow( 'Failed to load plugin' ); + expect(spy).toBeCalledWith( + chalk.red(`Failed to load plugin commitlint-plugin-nonexistentplugin.`) + ); + spy.mockRestore(); }); test('should load a scoped plugin when referenced by short name', () => { @@ -63,3 +86,40 @@ test("should load a scoped plugin when referenced by long name, but should not g const plugins = loadPlugin({}, '@scope/commitlint-plugin-example'); expect(plugins['example']).toBe(undefined); }); + +test('should load a plugin when relative posix path is provided', () => { + const plugins = loadPlugin({}, './relative/posix.js'); + expect(plugins['posix.js']).toBe(require('./relative/posix.js')); +}); + +test('should load a plugin when absolute posix path is provided', () => { + const plugins = loadPlugin({}, '/absolute/posix.js'); + // eslint-disable-next-line import/no-absolute-path + expect(plugins['posix.js']).toBe(require('/absolute/posix.js')); +}); + +if (platform() === 'win32') { + test('should load a plugin when relative windows path is provided', () => { + const plugins = loadPlugin({}, '.\\relative\\windows.js'); + expect(plugins['windows.js']).toBe(require('.\\relative\\windows.js')); + }); + + test('should load a plugin when absolute windows path is provided', () => { + const plugins = loadPlugin({}, 'C:\\absolute\\windows.js'); + // eslint-disable-next-line import/no-absolute-path + expect(plugins['windows.js']).toBe(require('C:\\absolute\\windows.js')); + }); +} else { + test('should not load a plugin when absolute windows path is provided', () => { + const spy = jest.spyOn(console, 'error').mockImplementation(); + expect(() => loadPlugin({}, 'C:\\absolute\\windows.js')).toThrow( + 'Failed to load plugin' + ); + expect(spy).toBeCalledWith( + chalk.red( + `Failed to load plugin commitlint-plugin-C:/absolute/windows.js.` + ) + ); + spy.mockRestore(); + }); +} diff --git a/@commitlint/load/src/utils/load-plugin.ts b/@commitlint/load/src/utils/load-plugin.ts index 30003816ba..4b387b4697 100644 --- a/@commitlint/load/src/utils/load-plugin.ts +++ b/@commitlint/load/src/utils/load-plugin.ts @@ -1,6 +1,6 @@ import path from 'path'; import chalk from 'chalk'; -import {normalizePackageName, getShorthandName} from './plugin-naming'; +import {normalizePackageName} from './plugin-naming'; import {WhitespacePluginError, MissingPluginError} from './plugin-errors'; import {PluginRecords} from '@commitlint/types'; @@ -9,8 +9,7 @@ export default function loadPlugin( pluginName: string, debug: boolean = false ): PluginRecords { - const longName = normalizePackageName(pluginName); - const shortName = getShorthandName(longName); + const {longName, shortName} = normalizePackageName(pluginName); let plugin = null; if (pluginName.match(/\s+/u)) { diff --git a/@commitlint/load/src/utils/plugin-naming.ts b/@commitlint/load/src/utils/plugin-naming.ts index ecf42784b4..784e8eaff5 100644 --- a/@commitlint/load/src/utils/plugin-naming.ts +++ b/@commitlint/load/src/utils/plugin-naming.ts @@ -1,27 +1,40 @@ import path from 'path'; -// largely adapted from eslint's plugin system -const NAMESPACE_REGEX = /^@.*\//iu; // In eslint this is a parameter - we don't need to support the extra options const prefix = 'commitlint-plugin'; -// Replace Windows with posix style paths -function convertPathToPosix(filepath: string) { +/** + * Replace Windows with posix style paths + */ +function convertPathToPosix(filepath: string): string { const normalizedFilepath = path.normalize(filepath); - const posixFilepath = normalizedFilepath.replace(/\\/gu, '/'); - - return posixFilepath; + return normalizedFilepath.replace(/\\/gu, '/'); } /** * Brings package name to correct format based on prefix - * @param {string} name The name of the package. - * @returns {string} Normalized name of the package - * @private + * @param name The name of the package. + * @returns Normalized name of the package + * @internal */ -export function normalizePackageName(name: string) { +export function normalizePackageName( + name: string +): {longName: string; shortName: string} { let normalizedName = name; + if ( + path.isAbsolute(name) || + name.startsWith('./') || + name.startsWith('../') || + name.startsWith('.\\') || + name.startsWith('..\\') + ) { + return { + longName: name, + shortName: path.basename(name) || name, + }; + } + /** * On Windows, name can come in with Windows slashes instead of Unix slashes. * Normalize to Unix first to avoid errors later on. @@ -61,40 +74,33 @@ export function normalizePackageName(name: string) { normalizedName = `${prefix}-${normalizedName}`; } - return normalizedName; + return { + longName: normalizedName, + shortName: getShorthandName(normalizedName), + }; } /** - * Removes the prefix from a fullname. - * @param {string} fullname The term which may have the prefix. - * @returns {string} The term without prefix. + * Removes the prefix from a fullName. + * @param fullName The term which may have the prefix. + * @returns The term without prefix. + * @internal */ -export function getShorthandName(fullname: string) { - if (fullname[0] === '@') { - let matchResult = new RegExp(`^(@[^/]+)/${prefix}$`, 'u').exec(fullname); +export function getShorthandName(fullName: string): string { + if (fullName[0] === '@') { + let matchResult = new RegExp(`^(@[^/]+)/${prefix}$`, 'u').exec(fullName); if (matchResult) { return matchResult[1]; } - matchResult = new RegExp(`^(@[^/]+)/${prefix}-(.+)$`, 'u').exec(fullname); + matchResult = new RegExp(`^(@[^/]+)/${prefix}-(.+)$`, 'u').exec(fullName); if (matchResult) { return `${matchResult[1]}/${matchResult[2]}`; } - } else if (fullname.startsWith(`${prefix}-`)) { - return fullname.slice(prefix.length + 1); + } else if (fullName.startsWith(`${prefix}-`)) { + return fullName.slice(prefix.length + 1); } - return fullname; -} - -/** - * Gets the scope (namespace) of a term. - * @param {string} term The term which may have the namespace. - * @returns {string} The namepace of the term if it has one. - */ -export function getNamespaceFromTerm(term: string) { - const match = term.match(NAMESPACE_REGEX); - - return match ? match[0] : ''; + return fullName; }