Skip to content

Commit 1a982fa

Browse files
committed
fix: resolve plugins correctly if provided as relative or absolute path
1 parent c2c257d commit 1a982fa

File tree

3 files changed

+86
-37
lines changed

3 files changed

+86
-37
lines changed

@commitlint/load/src/utils/load-plugin.test.ts

+45-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import loadPlugin from './load-plugin';
2+
import chalk from 'chalk';
23

34
jest.mock('commitlint-plugin-example', () => ({example: true}), {
45
virtual: true,
@@ -8,6 +9,22 @@ jest.mock('@scope/commitlint-plugin-example', () => ({scope: true}), {
89
virtual: true,
910
});
1011

12+
jest.mock('./relative/posix.js', () => ({relativePosix: true}), {
13+
virtual: true,
14+
});
15+
16+
jest.mock('/absolute/posix.js', () => ({relativePosix: true}), {
17+
virtual: true,
18+
});
19+
20+
jest.mock('.\\relative\\windows.js', () => ({relativePosix: true}), {
21+
virtual: true,
22+
});
23+
24+
jest.mock('C:\\absolute\\windows.js', () => ({relativePosix: true}), {
25+
virtual: true,
26+
});
27+
1128
test('should load a plugin when referenced by short name', () => {
1229
const plugins = loadPlugin({}, 'example');
1330
expect(plugins['example']).toBe(require('commitlint-plugin-example'));
@@ -34,9 +51,14 @@ test('should throw an error when a plugin has whitespace', () => {
3451
});
3552

3653
test("should throw an error when a plugin doesn't exist", () => {
54+
const spy = jest.spyOn(console, 'error').mockImplementation();
3755
expect(() => loadPlugin({}, 'nonexistentplugin')).toThrow(
38-
'Failed to load plugin'
56+
'Failed to load plugin commitlint-plugin-nonexistentplugin.'
57+
);
58+
expect(spy).toBeCalledWith(
59+
chalk.red(`Failed to load plugin commitlint-plugin-nonexistentplugin.`)
3960
);
61+
spy.mockRestore();
4062
});
4163

4264
test('should load a scoped plugin when referenced by short name', () => {
@@ -63,3 +85,25 @@ test("should load a scoped plugin when referenced by long name, but should not g
6385
const plugins = loadPlugin({}, '@scope/commitlint-plugin-example');
6486
expect(plugins['example']).toBe(undefined);
6587
});
88+
89+
test('should load a plugin when relative posix path is provided', () => {
90+
const plugins = loadPlugin({}, './relative/posix.js');
91+
expect(plugins['posix.js']).toBe(require('./relative/posix.js'));
92+
});
93+
94+
test('should load a plugin when absolute posix path is provided', () => {
95+
const plugins = loadPlugin({}, '/absolute/posix.js');
96+
// eslint-disable-next-line import/no-absolute-path
97+
expect(plugins['posix.js']).toBe(require('/absolute/posix.js'));
98+
});
99+
100+
test('should load a plugin when relative windows path is provided', () => {
101+
const plugins = loadPlugin({}, '.\\relative\\windows.js');
102+
expect(plugins['windows.js']).toBe(require('.\\relative\\windows.js'));
103+
});
104+
105+
test('should load a plugin when absolute windows path is provided', () => {
106+
const plugins = loadPlugin({}, 'C:\\absolute\\windows.js');
107+
// eslint-disable-next-line import/no-absolute-path
108+
expect(plugins['windows.js']).toBe(require('C:\\absolute\\windows.js'));
109+
});

@commitlint/load/src/utils/load-plugin.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import path from 'path';
22
import chalk from 'chalk';
3-
import {normalizePackageName, getShorthandName} from './plugin-naming';
3+
import {normalizePackageName} from './plugin-naming';
44
import {WhitespacePluginError, MissingPluginError} from './plugin-errors';
55
import {PluginRecords} from '@commitlint/types';
66

@@ -9,8 +9,7 @@ export default function loadPlugin(
99
pluginName: string,
1010
debug: boolean = false
1111
): PluginRecords {
12-
const longName = normalizePackageName(pluginName);
13-
const shortName = getShorthandName(longName);
12+
const {longName, shortName} = normalizePackageName(pluginName);
1413
let plugin = null;
1514

1615
if (pluginName.match(/\s+/u)) {
+39-33
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
11
import path from 'path';
22

3-
// largely adapted from eslint's plugin system
4-
const NAMESPACE_REGEX = /^@.*\//iu;
53
// In eslint this is a parameter - we don't need to support the extra options
64
const prefix = 'commitlint-plugin';
75

8-
// Replace Windows with posix style paths
9-
function convertPathToPosix(filepath: string) {
6+
/**
7+
* Replace Windows with posix style paths
8+
*/
9+
function convertPathToPosix(filepath: string): string {
1010
const normalizedFilepath = path.normalize(filepath);
11-
const posixFilepath = normalizedFilepath.replace(/\\/gu, '/');
12-
13-
return posixFilepath;
11+
return normalizedFilepath.replace(/\\/gu, '/');
1412
}
1513

1614
/**
1715
* Brings package name to correct format based on prefix
18-
* @param {string} name The name of the package.
19-
* @returns {string} Normalized name of the package
20-
* @private
16+
* @param name The name of the package.
17+
* @returns Normalized name of the package
18+
* @internal
2119
*/
22-
export function normalizePackageName(name: string) {
20+
export function normalizePackageName(
21+
name: string
22+
): {longName: string; shortName: string} {
2323
let normalizedName = name;
2424

25+
if (
26+
path.isAbsolute(name) ||
27+
name.startsWith('./') ||
28+
name.startsWith('../') ||
29+
name.startsWith('.\\') ||
30+
name.startsWith('..\\')
31+
) {
32+
return {
33+
longName: name,
34+
shortName: path.basename(name) || name,
35+
};
36+
}
37+
2538
/**
2639
* On Windows, name can come in with Windows slashes instead of Unix slashes.
2740
* Normalize to Unix first to avoid errors later on.
@@ -61,40 +74,33 @@ export function normalizePackageName(name: string) {
6174
normalizedName = `${prefix}-${normalizedName}`;
6275
}
6376

64-
return normalizedName;
77+
return {
78+
longName: normalizedName,
79+
shortName: getShorthandName(normalizedName),
80+
};
6581
}
6682

6783
/**
68-
* Removes the prefix from a fullname.
69-
* @param {string} fullname The term which may have the prefix.
70-
* @returns {string} The term without prefix.
84+
* Removes the prefix from a fullName.
85+
* @param fullName The term which may have the prefix.
86+
* @returns The term without prefix.
87+
* @internal
7188
*/
72-
export function getShorthandName(fullname: string) {
73-
if (fullname[0] === '@') {
74-
let matchResult = new RegExp(`^(@[^/]+)/${prefix}$`, 'u').exec(fullname);
89+
export function getShorthandName(fullName: string): string {
90+
if (fullName[0] === '@') {
91+
let matchResult = new RegExp(`^(@[^/]+)/${prefix}$`, 'u').exec(fullName);
7592

7693
if (matchResult) {
7794
return matchResult[1];
7895
}
7996

80-
matchResult = new RegExp(`^(@[^/]+)/${prefix}-(.+)$`, 'u').exec(fullname);
97+
matchResult = new RegExp(`^(@[^/]+)/${prefix}-(.+)$`, 'u').exec(fullName);
8198
if (matchResult) {
8299
return `${matchResult[1]}/${matchResult[2]}`;
83100
}
84-
} else if (fullname.startsWith(`${prefix}-`)) {
85-
return fullname.slice(prefix.length + 1);
101+
} else if (fullName.startsWith(`${prefix}-`)) {
102+
return fullName.slice(prefix.length + 1);
86103
}
87104

88-
return fullname;
89-
}
90-
91-
/**
92-
* Gets the scope (namespace) of a term.
93-
* @param {string} term The term which may have the namespace.
94-
* @returns {string} The namepace of the term if it has one.
95-
*/
96-
export function getNamespaceFromTerm(term: string) {
97-
const match = term.match(NAMESPACE_REGEX);
98-
99-
return match ? match[0] : '';
105+
return fullName;
100106
}

0 commit comments

Comments
 (0)