Skip to content

Commit 1a8db80

Browse files
committed
feat: add function rule plugin sources and tests
1 parent 1f38e86 commit 1a8db80

10 files changed

+254
-4
lines changed

package-lock.json

+1-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+12-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
"name": "commitlint-plugin-function-rules",
33
"version": "1.0.0",
44
"description": "Commitlint plugin to define rules as functions.",
5-
"private": true,
5+
"main": "dist/index.js",
6+
"types": "dist/index.d.js",
7+
"files": [
8+
"dist/**/!(*.test).{js,d.ts}"
9+
],
610
"scripts": {
711
"lint": "run-p format:check lint-es",
812
"lint:fix": "run-s format lint-es:fix",
@@ -34,10 +38,16 @@
3438
"url": "https://github.com/vidavidorra/commitlint-plugin-function-rules/issues"
3539
},
3640
"homepage": "https://github.com/vidavidorra/commitlint-plugin-function-rules#readme",
37-
"dependencies": {},
41+
"peerDependencies": {
42+
"@commitlint/lint": "9.1.2"
43+
},
44+
"dependencies": {
45+
"@commitlint/types": "9.1.2"
46+
},
3847
"devDependencies": {
3948
"@commitlint/cli": "9.1.2",
4049
"@commitlint/config-conventional": "9.1.2",
50+
"@commitlint/rules": "9.1.2",
4151
"@jest/globals": "26.4.2",
4252
"@semantic-release/changelog": "5.0.1",
4353
"@semantic-release/git": "9.0.0",

src/function-rule.test.ts

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { Commit, RuleConfigCondition, RuleOutcome } from '@commitlint/types';
2+
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
3+
import functionRule, { FunctionRule } from './function-rule';
4+
5+
describe('functionRule', (): void => {
6+
const commit: Commit = {
7+
type: 'chore',
8+
scope: 'scope',
9+
subject: 'test',
10+
merge: null,
11+
header: 'chore(scope): test',
12+
body: null,
13+
footer: null,
14+
notes: [],
15+
references: [],
16+
mentions: [],
17+
revert: null,
18+
raw: 'chore(scope): test\n',
19+
};
20+
const when: RuleConfigCondition = 'always';
21+
/**
22+
* To pass this to an function, that is obviously not expecting a mock, a type
23+
* assertion is needed. For this, the as-syntax is needed when a mock is
24+
* passed as function argument.
25+
*/
26+
const ruleImplementation = jest.fn();
27+
28+
beforeEach(() => {
29+
ruleImplementation.mockReset();
30+
});
31+
32+
it('calls implementation function', () => {
33+
functionRule(commit, when, ruleImplementation as FunctionRule);
34+
35+
expect(ruleImplementation).toHaveBeenCalledTimes(1);
36+
});
37+
38+
it('passes arguments to implementation function', () => {
39+
functionRule(commit, when, ruleImplementation as FunctionRule);
40+
41+
expect(ruleImplementation).toHaveBeenCalledWith(commit, when);
42+
});
43+
44+
it("defaults 'when' argument to 'always'", () => {
45+
functionRule(commit, undefined, ruleImplementation as FunctionRule);
46+
47+
expect(ruleImplementation).toHaveBeenCalledWith(commit, 'always');
48+
});
49+
50+
it('returns value from sync implementation function', () => {
51+
const returnValue: RuleOutcome = [
52+
true,
53+
'Message from sync implementation function.',
54+
];
55+
ruleImplementation.mockImplementation(() => returnValue);
56+
const value = functionRule(
57+
commit,
58+
when,
59+
ruleImplementation as FunctionRule,
60+
);
61+
62+
expect(ruleImplementation).toHaveBeenCalledTimes(1);
63+
expect(value).toEqual(returnValue);
64+
});
65+
66+
it('returns value from async implementation function', async () => {
67+
const returnValue: RuleOutcome = [
68+
true,
69+
'Message from async implementation function.',
70+
];
71+
ruleImplementation.mockImplementation(() => Promise.resolve(returnValue));
72+
const value = await functionRule(
73+
commit,
74+
when,
75+
ruleImplementation as FunctionRule,
76+
);
77+
78+
expect(ruleImplementation).toHaveBeenCalledTimes(1);
79+
expect(value).toEqual(returnValue);
80+
});
81+
82+
it("throws an error when 'value' is 'undefined'", () => {
83+
expect(() => {
84+
functionRule(commit, when, undefined);
85+
}).toThrow();
86+
});
87+
88+
it("throws an error when 'value' is 'not an function'", () => {
89+
expect(() => {
90+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
91+
// @ts-expect-error: TS2345: Argument of type ... is not assignable to parameter of type ...
92+
functionRule(commit, when, 'not a function!');
93+
}).toThrow();
94+
});
95+
});

src/function-rule.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {
2+
Commit,
3+
Rule,
4+
RuleConfigCondition,
5+
RuleOutcome,
6+
} from '@commitlint/types';
7+
8+
type FunctionRule = (
9+
parsed: Commit,
10+
when: RuleConfigCondition,
11+
) => RuleOutcome | Promise<RuleOutcome>;
12+
13+
const functionRule: Rule<FunctionRule> = (
14+
parsed: Commit,
15+
when: RuleConfigCondition = 'always',
16+
value: FunctionRule | undefined,
17+
) => {
18+
if (typeof value === 'function') {
19+
return value(parsed, when);
20+
}
21+
22+
throw new Error('Not a valid function!');
23+
};
24+
25+
export default functionRule;
26+
export { FunctionRule };

src/index.test.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as plugin from './index';
2+
import { describe, expect, it } from '@jest/globals';
3+
import rules from './rules';
4+
5+
describe('index', () => {
6+
it(`exports a CommonJS module with 'rules' object`, () => {
7+
expect(Object.keys(plugin)).toEqual(['rules']);
8+
expect(typeof plugin.rules).toEqual('object');
9+
});
10+
11+
it('exports rules', () => {
12+
expect(plugin).toMatchObject({ rules });
13+
});
14+
});

src/index.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Plugin from './plugin';
2+
import rules from './rules';
3+
4+
const plugin: Plugin = {
5+
rules,
6+
};
7+
8+
/**
9+
* Export single object, according to the CommonJS model. The CommonJS module is
10+
* explicitly used here as that's the kind of module commitlint requires for
11+
* plugins.
12+
*/
13+
export = plugin;

src/plugin.test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { describe, expect, it } from '@jest/globals';
2+
import Plugin from './plugin';
3+
4+
describe('Plugin', (): void => {
5+
it("defines 'rules' as an object", () => {
6+
const plugin: Plugin = {
7+
rules: {},
8+
};
9+
10+
expect(Object.keys(plugin)).toEqual(['rules']);
11+
expect(typeof plugin.rules).toEqual('object');
12+
});
13+
});

src/plugin.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { Rules } from './rules';
2+
3+
interface Plugin {
4+
rules: Rules;
5+
}
6+
7+
export default Plugin;

src/rules.test.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, expect, it } from '@jest/globals';
2+
import commitlintRules from '@commitlint/rules';
3+
import functionRule from './function-rule';
4+
import rules from './rules';
5+
6+
describe('rules', (): void => {
7+
const prefix = 'function-rules/';
8+
const names = Object.keys(rules);
9+
const commitlintRuleNames = Object.keys(commitlintRules).sort();
10+
11+
it('exports the same rules as commitlint', () => {
12+
const strippedNames = names.map((e) => {
13+
return e.replace(new RegExp(`^${prefix}`), '');
14+
});
15+
16+
expect(strippedNames).toEqual(commitlintRuleNames);
17+
});
18+
19+
it(`are exported with with '${prefix}' as prefix`, () => {
20+
const everyNameHasPrefix = names.every((e) => e.startsWith(prefix));
21+
22+
expect(everyNameHasPrefix).toBe(true);
23+
});
24+
25+
it(`have 'functionRule' as value`, () => {
26+
const values = Object.values(rules);
27+
const everyValueIsFunctionRule = values.every((e) => e === functionRule);
28+
29+
expect(everyValueIsFunctionRule).toBe(true);
30+
});
31+
});

src/rules.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import functionRule, { FunctionRule } from './function-rule';
2+
import { Rule } from '@commitlint/types';
3+
4+
type Rules = Record<string, Rule<FunctionRule>>;
5+
6+
const rules: Rules = {
7+
'function-rules/body-case': functionRule,
8+
'function-rules/body-empty': functionRule,
9+
'function-rules/body-leading-blank': functionRule,
10+
'function-rules/body-max-length': functionRule,
11+
'function-rules/body-max-line-length': functionRule,
12+
'function-rules/body-min-length': functionRule,
13+
'function-rules/footer-empty': functionRule,
14+
'function-rules/footer-leading-blank': functionRule,
15+
'function-rules/footer-max-length': functionRule,
16+
'function-rules/footer-max-line-length': functionRule,
17+
'function-rules/footer-min-length': functionRule,
18+
'function-rules/header-case': functionRule,
19+
'function-rules/header-full-stop': functionRule,
20+
'function-rules/header-max-length': functionRule,
21+
'function-rules/header-min-length': functionRule,
22+
'function-rules/references-empty': functionRule,
23+
'function-rules/scope-case': functionRule,
24+
'function-rules/scope-empty': functionRule,
25+
'function-rules/scope-enum': functionRule,
26+
'function-rules/scope-max-length': functionRule,
27+
'function-rules/scope-min-length': functionRule,
28+
'function-rules/signed-off-by': functionRule,
29+
'function-rules/subject-case': functionRule,
30+
'function-rules/subject-empty': functionRule,
31+
'function-rules/subject-full-stop': functionRule,
32+
'function-rules/subject-max-length': functionRule,
33+
'function-rules/subject-min-length': functionRule,
34+
'function-rules/type-case': functionRule,
35+
'function-rules/type-empty': functionRule,
36+
'function-rules/type-enum': functionRule,
37+
'function-rules/type-max-length': functionRule,
38+
'function-rules/type-min-length': functionRule,
39+
};
40+
41+
export default rules;
42+
export { Rules };

0 commit comments

Comments
 (0)