Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(lint): port lint to typescript #908

Merged
merged 9 commits into from Feb 5, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions @commitlint/ensure/package.json
Expand Up @@ -38,6 +38,7 @@
"globby": "11.0.0"
},
"dependencies": {
"@commitlint/types": "^8.3.4",
Copy link
Contributor

@armano2 armano2 Feb 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@commitlint/types can/should be in devDependencies as it does not contain any runtime code

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our case it does - we expose som enums, which emit runtime code, e.g.

// @commitlint/types/lib/load.js
"use strict";
exports.__esModule = true;
var RuleSeverity;
(function (RuleSeverity) {
    RuleSeverity[RuleSeverity["Disabled"] = 0] = "Disabled";
    RuleSeverity[RuleSeverity["Warning"] = 1] = "Warning";
    RuleSeverity[RuleSeverity["Error"] = 2] = "Error";
})(RuleSeverity = exports.RuleSeverity || (exports.RuleSeverity = {}));
var RuleConfigQuality;
(function (RuleConfigQuality) {
    RuleConfigQuality[RuleConfigQuality["User"] = 0] = "User";
    RuleConfigQuality[RuleConfigQuality["Qualified"] = 1] = "Qualified";
})(RuleConfigQuality = exports.RuleConfigQuality || (exports.RuleConfigQuality = {}));
//# sourceMappingURL=load.js.map

"lodash": "^4.17.15"
}
}
2 changes: 1 addition & 1 deletion @commitlint/ensure/src/case.ts
Expand Up @@ -3,7 +3,7 @@ import kebabCase from 'lodash/kebabCase';
import snakeCase from 'lodash/snakeCase';
import upperFirst from 'lodash/upperFirst';
import startCase from 'lodash/startCase';
import {TargetCaseType} from '.';
import {TargetCaseType} from '@commitlint/types';

export default ensureCase;

Expand Down
1 change: 0 additions & 1 deletion @commitlint/ensure/src/index.ts
Expand Up @@ -5,7 +5,6 @@ import maxLineLength from './max-line-length';
import minLength from './min-length';
import notEmpty from './not-empty';

export * from './types';
export {ensureCase as case};
export {ensureEnum as enum};
export {maxLength, maxLineLength, minLength, notEmpty};
3 changes: 2 additions & 1 deletion @commitlint/ensure/tsconfig.json
Expand Up @@ -6,5 +6,6 @@
"outDir": "./lib"
},
"include": ["./src/**/*.ts"],
"exclude": ["./src/**/*.test.ts", "./lib/**/*"]
"exclude": ["./src/**/*.test.ts", "./lib/**/*"],
"references": [{ "path": "../types" }]
}
1 change: 1 addition & 0 deletions @commitlint/is-ignored/package.json
Expand Up @@ -40,6 +40,7 @@
"@types/semver": "7.1.0"
},
"dependencies": {
"@commitlint/types": "^8.3.4",
"semver": "7.1.2"
}
}
2 changes: 1 addition & 1 deletion @commitlint/is-ignored/src/defaults.ts
@@ -1,5 +1,5 @@
import * as semver from 'semver';
import {Matcher} from './types';
import {Matcher} from '@commitlint/types';

const isSemver = (c: string): boolean => {
const firstLine = c.split('\n').shift();
Expand Down
2 changes: 1 addition & 1 deletion @commitlint/is-ignored/src/index.ts
@@ -1,2 +1,2 @@
export * from './is-ignored';
export * from './types';
export {default} from './is-ignored';
7 changes: 1 addition & 6 deletions @commitlint/is-ignored/src/is-ignored.ts
@@ -1,10 +1,5 @@
import {wildcards} from './defaults';
import {Matcher} from './types';

export interface IsIgnoredOptions {
ignores?: Matcher[];
defaults?: boolean;
}
import {IsIgnoredOptions} from '@commitlint/types';

export default function isIgnored(
commit: string = '',
Expand Down
1 change: 0 additions & 1 deletion @commitlint/is-ignored/src/types.ts

This file was deleted.

3 changes: 2 additions & 1 deletion @commitlint/is-ignored/tsconfig.json
Expand Up @@ -11,5 +11,6 @@
"exclude": [
"./src/**/*.test.ts",
"./lib/**/*"
]
],
"references": [{"path": "../types"}]
}
14 changes: 4 additions & 10 deletions @commitlint/lint/package.json
Expand Up @@ -2,21 +2,14 @@
"name": "@commitlint/lint",
"version": "8.3.5",
"description": "Lint a string against commitlint rules",
"main": "lib/index.js",
"main": "lib/lint.js",
"types": "lib/lint.d.ts",
"files": [
"lib/"
],
"scripts": {
"build": "cross-env NODE_ENV=production babel src --out-dir lib --source-maps",
"deps": "dep-check",
"pkg": "pkg-check --skip-import",
"start": "yarn run watch",
"watch": "babel src --out-dir lib --watch --source-maps"
},
"babel": {
"presets": [
"babel-preset-commitlint"
]
"pkg": "pkg-check --skip-import"
},
"engines": {
"node": ">=4"
Expand Down Expand Up @@ -53,6 +46,7 @@
"@commitlint/is-ignored": "^8.3.5",
"@commitlint/parse": "^8.3.4",
"@commitlint/rules": "^8.3.4",
"@commitlint/types": "^8.3.4",
"lodash": "^4.17.15"
}
}
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
85 changes: 50 additions & 35 deletions @commitlint/lint/src/index.js → @commitlint/lint/src/lint.ts
@@ -1,20 +1,29 @@
import util from 'util';
import isIgnored from '@commitlint/is-ignored';
import parse from '@commitlint/parse';
import implementations from '@commitlint/rules';
import defaultRules from '@commitlint/rules';
import toPairs from 'lodash/toPairs';
import values from 'lodash/values';
import {buildCommitMesage} from './commit-message';
import {
LintRuleConfig,
LintOptions,
LintRuleOutcome,
Rule,
Plugin,
RuleSeverity
} from '@commitlint/types';

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 +38,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 +76,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 +92,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 +126,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 +144,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 +160,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 +175,4 @@ export default async (message, rules = {}, opts = {}) => {
warnings,
input: buildCommitMesage(parsed)
};
};
}
21 changes: 21 additions & 0 deletions @commitlint/lint/tsconfig.json
@@ -0,0 +1,21 @@
{
"extends": "../../tsconfig.shared.json",
"compilerOptions": {
"composite": true,
"rootDir": "./src",
"outDir": "./lib"
},
"include": [
"./src"
],
"exclude": [
"./src/**/*.test.ts",
"./lib/**/*"
],
"references": [
{"path": "../is-ignored"},
{"path": "../parse"},
{"path": "../rules"},
{"path": "../types"}
]
}
1 change: 1 addition & 0 deletions @commitlint/load/package.json
Expand Up @@ -41,6 +41,7 @@
"dependencies": {
"@commitlint/execute-rule": "^8.3.4",
"@commitlint/resolve-extends": "^8.3.5",
"@commitlint/types": "^8.3.5",
"chalk": "3.0.0",
"cosmiconfig": "^6.0.0",
"lodash": "^4.17.15",
Expand Down