Skip to content

Commit

Permalink
refactor: port to ts
Browse files Browse the repository at this point in the history
  • Loading branch information
marionebl committed Feb 5, 2020
1 parent 7bf13b4 commit 6169610
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 71 deletions.
4 changes: 2 additions & 2 deletions @commitlint/lint/package.json
Expand Up @@ -2,8 +2,8 @@
"name": "@commitlint/lint",
"version": "8.3.5",
"description": "Lint a string against commitlint rules",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"main": "lib/lint.js",
"types": "lib/lint.d.ts",
"files": [
"lib/"
],
Expand Down
18 changes: 18 additions & 0 deletions @commitlint/lint/src/commit-message.ts
@@ -0,0 +1,18 @@
export interface CommitMessageData {
header: string;
body?: string | null;
footer?: string | null;
}

export const buildCommitMesage = ({
header,
body,
footer
}: CommitMessageData): string => {
let message = header;

message = body ? `${message}\n\n${body}` : message;
message = footer ? `${message}\n\n${footer}` : message;

return message;
};
@@ -1,12 +1,12 @@
import lint from '.';
import lint from './lint';

test('throws without params', async () => {
const error = lint();
const error = (lint as any)();
await expect(error).rejects.toThrow('Expected a raw commit');
});

test('throws with empty message', async () => {
const error = lint('');
const error = (lint as any)('');
await expect(error).rejects.toThrow('Expected a raw commit');
});

Expand Down Expand Up @@ -91,7 +91,7 @@ test('throws for invalid rule config', async () => {
const error = lint('type(scope): foo', {
'type-enum': 1,
'scope-enum': {0: 2, 1: 'never', 2: ['foo'], length: 3}
});
} as any);

await expect(error).rejects.toThrow('type-enum must be array');
await expect(error).rejects.toThrow('scope-enum must be array');
Expand All @@ -109,15 +109,15 @@ test('allows disable shorthand', async () => {
});

test('throws for rule with invalid length', async () => {
const error = lint('type(scope): foo', {'scope-enum': [1, 2, 3, 4]});
const error = lint('type(scope): foo', {'scope-enum': [1, 2, 3, 4]} as any);

await expect(error).rejects.toThrow('scope-enum must be 2 or 3 items long');
});

test('throws for rule with invalid level', async () => {
const error = lint('type(scope): foo', {
'type-enum': ['2', 'always'],
'header-max-length': [{}, 'always']
'type-enum': ['2', 'always'] as any,
'header-max-length': [{}, 'always'] as any
});
await expect(error).rejects.toThrow('rule type-enum must be number');
await expect(error).rejects.toThrow('rule header-max-length must be number');
Expand All @@ -137,8 +137,8 @@ test('throws for rule with out of range level', async () => {

test('throws for rule with invalid condition', async () => {
const error = lint('type(scope): foo', {
'type-enum': [1, 2],
'header-max-length': [1, {}]
'type-enum': [1, 2] as any,
'header-max-length': [1, {}] as any
});

await expect(error).rejects.toThrow('type-enum must be string');
Expand All @@ -147,8 +147,8 @@ test('throws for rule with invalid condition', async () => {

test('throws for rule with out of range condition', async () => {
const error = lint('type(scope): foo', {
'type-enum': [1, 'foo'],
'header-max-length': [1, 'bar']
'type-enum': [1, 'foo'] as any,
'header-max-length': [1, 'bar'] as any
});

await expect(error).rejects.toThrow('type-enum must be "always" or "never"');
Expand Down
79 changes: 44 additions & 35 deletions @commitlint/lint/src/index.js → @commitlint/lint/src/lint.ts
@@ -1,20 +1,23 @@
import util from 'util';
import isIgnored from '@commitlint/is-ignored';
import parse from '@commitlint/parse';
import implementations from '@commitlint/rules';
import defaultRules, {Rule} from '@commitlint/rules';
import toPairs from 'lodash/toPairs';
import values from 'lodash/values';
import {buildCommitMesage} from './commit-message';
import {LintRuleConfig, LintOptions, LintRuleOutcome} from './types';
import {Plugin, RuleSeverity} from '@commitlint/load';

export default async function lint(
message: string,
rawRulesConfig?: LintRuleConfig,
rawOpts?: LintOptions
) {
const opts = rawOpts
? rawOpts
: {defaultIgnores: undefined, ignores: undefined};
const rulesConfig = rawRulesConfig || {};

const buildCommitMesage = ({header, body, footer}) => {
let message = header;

message = body ? `${message}\n\n${body}` : message;
message = footer ? `${message}\n\n${footer}` : message;

return message;
};

export default async (message, rules = {}, opts = {}) => {
// Found a wildcard match, skip
if (
isIgnored(message, {defaults: opts.defaultIgnores, ignores: opts.ignores})
Expand All @@ -29,33 +32,35 @@ export default async (message, rules = {}, opts = {}) => {

// Parse the commit message
const parsed = await parse(message, undefined, opts.parserOpts);
const allRules: Map<string, Rule<unknown> | Rule<never>> = new Map(
Object.entries(defaultRules)
);

const mergedImplementations = Object.assign({}, implementations);
if (opts.plugins) {
values(opts.plugins).forEach(plugin => {
values(opts.plugins).forEach((plugin: Plugin) => {
if (plugin.rules) {
Object.keys(plugin.rules).forEach(ruleKey => {
mergedImplementations[ruleKey] = plugin.rules[ruleKey];
});
Object.keys(plugin.rules).forEach(ruleKey =>
allRules.set(ruleKey, plugin.rules[ruleKey])
);
}
});
}

// Find invalid rules configs
const missing = Object.keys(rules).filter(
name => typeof mergedImplementations[name] !== 'function'
const missing = Object.keys(rulesConfig).filter(
name => typeof allRules.get(name) !== 'function'
);

if (missing.length > 0) {
const names = Object.keys(mergedImplementations);
const names = [...allRules.keys()];
throw new RangeError(
`Found invalid rule names: ${missing.join(
', '
)}. Supported rule names are: ${names.join(', ')}`
);
}

const invalid = toPairs(rules)
const invalid = toPairs(rulesConfig)
.map(([name, config]) => {
if (!Array.isArray(config)) {
return new Error(
Expand All @@ -65,7 +70,13 @@ export default async (message, rules = {}, opts = {}) => {
);
}

const [level, when] = config;
const [level] = config;

if (level === RuleSeverity.Disabled && config.length === 1) {
return null;
}

const [, when] = config;

if (typeof level !== 'number' || isNaN(level)) {
return new Error(
Expand All @@ -75,10 +86,6 @@ export default async (message, rules = {}, opts = {}) => {
);
}

if (level === 0 && config.length === 1) {
return null;
}

if (config.length !== 2 && config.length !== 3) {
return new Error(
`config for rule ${name} must be 2 or 3 items long, received ${util.inspect(
Expand Down Expand Up @@ -113,18 +120,15 @@ export default async (message, rules = {}, opts = {}) => {

return null;
})
.filter(item => item instanceof Error);
.filter((item): item is Error => item instanceof Error);

if (invalid.length > 0) {
throw new Error(invalid.map(i => i.message).join('\n'));
}

// Validate against all rules
const results = toPairs(rules)
.filter(entry => {
const [, [level]] = toPairs(entry);
return level > 0;
})
const results = toPairs(rulesConfig)
.filter(([, [level]]) => level > 0)
.map(entry => {
const [name, config] = entry;
const [level, when, value] = config;
Expand All @@ -134,9 +138,14 @@ export default async (message, rules = {}, opts = {}) => {
return null;
}

const rule = mergedImplementations[name];
const rule = allRules.get(name);

if (!rule) {
throw new Error(`Could not find rule implementation for ${name}`);
}

const [valid, message] = rule(parsed, when, value);
const executableRule = rule as Rule<unknown>;
const [valid, message] = executableRule(parsed, when, value);

return {
level,
Expand All @@ -145,7 +154,7 @@ export default async (message, rules = {}, opts = {}) => {
message
};
})
.filter(Boolean);
.filter((result): result is LintRuleOutcome => result !== null);

const errors = results.filter(result => result.level === 2 && !result.valid);
const warnings = results.filter(
Expand All @@ -160,4 +169,4 @@ export default async (message, rules = {}, opts = {}) => {
warnings,
input: buildCommitMesage(parsed)
};
};
}
23 changes: 10 additions & 13 deletions @commitlint/lint/src/types.ts
@@ -1,26 +1,23 @@
import {IsIgnoredOptions} from '@commitlint/is-ignored';
import {RuleConfigTuple, PluginRecords, RuleSeverity} from '@commitlint/load';
import {ParserOptions} from '@commitlint/parse';
import {Rule} from '@commitlint/rules';

export type Linter = (
commit: string,
config: LinterRuleConfig,
options: LinterOptions
) => Promise<LintOutcome>;
export type LintRuleConfig = Record<
string,
| Readonly<[RuleSeverity.Disabled]>
| RuleConfigTuple<void>
| RuleConfigTuple<unknown>
>;

export type LinterRuleConfig = {
[key: string]: any; // todo: use rule configuration from `@commitlint/load`
};

export interface LinterOptions {
export interface LintOptions {
/** If it should ignore the default commit messages (defaults to `true`) */
defaultIgnores?: IsIgnoredOptions['defaults'];
/** Additional commits to ignore, defined by ignore matchers */
ignores?: IsIgnoredOptions['ignores'];
/** The parser configuration to use when linting the commit */
parserOpts?: ParserOptions;

plugins?: any; // todo: reuse types from `@commitlint/load`
plugins?: PluginRecords;
}

export interface LintOutcome {
Expand All @@ -42,5 +39,5 @@ export interface LintRuleOutcome {
/** The name of the rule */
name: string;
/** The message returned from the rule, if invalid */
message?: string;
message: string;
}
4 changes: 2 additions & 2 deletions @commitlint/load/package.json
Expand Up @@ -2,8 +2,8 @@
"name": "@commitlint/load",
"version": "8.3.5",
"description": "Load shared commitlint configuration",
"main": "lib/load.js",
"types": "lib/load.d.ts",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib/"
],
Expand Down
4 changes: 4 additions & 0 deletions @commitlint/load/src/index.ts
@@ -0,0 +1,4 @@
import load from './load';
export default load;

export * from './types';
22 changes: 15 additions & 7 deletions @commitlint/load/src/types.ts
@@ -1,24 +1,30 @@
import {TargetCaseType} from '@commitlint/ensure';
import {Rule} from '@commitlint/rules';

export type RuleCondition = 'always' | 'never';

export type PluginRecords = Record<string, unknown>;
export type PluginRecords = Record<string, Plugin>;

export interface Plugin {
rules: {
[ruleName: string]: Rule<unknown>;
};
}

export interface LoadOptions {
cwd?: string;
file?: string;
}

export enum RuleSeverity {
Disabled = 0,
Warning = 1,
Error = 2
}

export type RuleConfigTuple<T> = ReadonlyArray<
T extends void
? [RuleSeverity, RuleCondition]
: [RuleSeverity, RuleCondition, T]
>;
export type RuleConfigTuple<T> = T extends void
? Readonly<[RuleSeverity, RuleCondition]>
: Readonly<[RuleSeverity, RuleCondition, T]>;

export enum RuleConfigQuality {
User,
Expand All @@ -33,7 +39,9 @@ export type QualifiedRuleConfig<T> =
export type RuleConfig<
V = RuleConfigQuality.Qualified,
T = void
> = V extends false ? RuleConfigTuple<T> : QualifiedRuleConfig<T>;
> = V extends RuleConfigQuality.Qualified
? RuleConfigTuple<T>
: QualifiedRuleConfig<T>;

export type CaseRuleConfig<V = RuleConfigQuality.User> = RuleConfig<
V,
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.shared.json
Expand Up @@ -18,6 +18,7 @@
"keyofStringsOnly": true,
"noFallthroughCasesInSwitch": true,
"isolatedModules": true,
"skipLibCheck": true
"skipLibCheck": true,
"downlevelIteration": true
}
}

0 comments on commit 6169610

Please sign in to comment.