diff --git a/@commitlint/cli/src/cli.ts b/@commitlint/cli/src/cli.ts index 688cb0f1e4..eeec77be7d 100644 --- a/@commitlint/cli/src/cli.ts +++ b/@commitlint/cli/src/cli.ts @@ -8,7 +8,7 @@ import resolveGlobal from 'resolve-global'; import yargs from 'yargs'; import util from 'util'; -import {CliFlags, Seed} from './types'; +import {CliFlags} from './types'; import { LintOptions, LintOutcome, @@ -16,6 +16,7 @@ import { ParserPreset, QualifiedConfig, Formatter, + UserConfig, } from '@commitlint/types'; import {CliError} from './cli-error'; @@ -332,7 +333,7 @@ function getEditValue(flags: CliFlags) { return edit; } -function getSeed(flags: CliFlags): Seed { +function getSeed(flags: CliFlags): UserConfig { const n = (flags.extends || []).filter( (i): i is string => typeof i === 'string' ); diff --git a/@commitlint/cli/src/types.ts b/@commitlint/cli/src/types.ts index fd76023cc3..598c24a20b 100644 --- a/@commitlint/cli/src/types.ts +++ b/@commitlint/cli/src/types.ts @@ -18,8 +18,3 @@ export interface CliFlags { _: string[]; $0: string; } - -export interface Seed { - extends?: string[]; - parserPreset?: string; -} diff --git a/@commitlint/config-validator/package.json b/@commitlint/config-validator/package.json new file mode 100644 index 0000000000..267036fd33 --- /dev/null +++ b/@commitlint/config-validator/package.json @@ -0,0 +1,34 @@ +{ + "name": "@commitlint/config-validator", + "version": "11.0.0", + "description": "commitizen prompt using commitlint.config.js", + "main": "./lib/validate.js", + "files": [ + "lib/" + ], + "scripts": { + "deps": "dep-check", + "pkg": "pkg-check --skip-import" + }, + "repository": { + "type": "git", + "url": "https://github.com/conventional-changelog/commitlint.git" + }, + "author": "Mario Nebl ", + "license": "MIT", + "bugs": { + "url": "https://github.com/conventional-changelog/commitlint/issues" + }, + "homepage": "https://github.com/conventional-changelog/commitlint#readme", + "engines": { + "node": ">=v10" + }, + "devDependencies": { + "@commitlint/utils": "^11.0.0" + }, + "dependencies": { + "@commitlint/types": "^11.0.0", + "ajv": "^6.12.6" + }, + "gitHead": "cb565dfcca3128380b9b3dc274aedbcae34ce5ca" +} diff --git a/@commitlint/config-validator/src/__snapshots__/validate.test.ts.snap b/@commitlint/config-validator/src/__snapshots__/validate.test.ts.snap new file mode 100644 index 0000000000..dc21028dee --- /dev/null +++ b/@commitlint/config-validator/src/__snapshots__/validate.test.ts.snap @@ -0,0 +1,129 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`validation should fail for defaultIgnoresNotBoolean 1`] = ` +"Commitlint configuration in defaultIgnoresNotBoolean.js is invalid: + - Property \\"defaultIgnores\\" has the wrong type - should be boolean. +" +`; + +exports[`validation should fail for extendsAsObject 1`] = ` +"Commitlint configuration in extendsAsObject.js is invalid: + - Property \\"extends\\" has the wrong type - should be array. + - Property \\"extends\\" has the wrong type - should be string. + - \\"extends\\" should match exactly one schema in oneOf. Value: {\\"test\\":1}. +" +`; + +exports[`validation should fail for extendsWithFunction 1`] = ` +"Commitlint configuration in extendsWithFunction.js is invalid: + - Property \\"extends[0]\\" has the wrong type - should be string. + - Property \\"extends\\" has the wrong type - should be string. + - \\"extends\\" should match exactly one schema in oneOf. Value: [null]. +" +`; + +exports[`validation should fail for formatterAsObject 1`] = ` +"Commitlint configuration in formatterAsObject.js is invalid: + - Property \\"formatter\\" has the wrong type - should be string. +" +`; + +exports[`validation should fail for helpUrlAsArray 1`] = ` +"Commitlint configuration in helpUrlAsArray.js is invalid: + - Property \\"helpUrl\\" has the wrong type - should be string. +" +`; + +exports[`validation should fail for helpUrlNotString 1`] = ` +"Commitlint configuration in helpUrlNotString.js is invalid: + - Property \\"helpUrl\\" has the wrong type - should be string. +" +`; + +exports[`validation should fail for ignoresFunction 1`] = ` +"Commitlint configuration in ignoresFunction.js is invalid: + - Property \\"ignores\\" has the wrong type - should be array. +" +`; + +exports[`validation should fail for ignoresNotFunction 1`] = ` +"Commitlint configuration in ignoresNotFunction.js is invalid: + - \\"ignores[0]\\" should be a function. Value: 1. +" +`; + +exports[`validation should fail for parserPreset 1`] = ` +"Commitlint configuration in parserPreset.js is invalid: + - Property \\"parserPreset\\" has the wrong type - should be string. + - Property \\"parserPreset\\" has the wrong type - should be object. + - \\"parserPreset\\" should match exactly one schema in oneOf. Value: []. +" +`; + +exports[`validation should fail for pluginsNotArray 1`] = ` +"Commitlint configuration in pluginsNotArray.js is invalid: + - Property \\"plugins\\" has the wrong type - should be array. +" +`; + +exports[`validation should fail for rules1 1`] = ` +"Commitlint configuration in rules1.js is invalid: + - \\"rules['a'][0]\\" should be equal to one of the allowed values. Value: 3. + - \\"rules['a']\\" should match exactly one schema in oneOf. Value: [3]. +" +`; + +exports[`validation should fail for rules2 1`] = ` +"Commitlint configuration in rules2.js is invalid: + - \\"rules['b']\\" should NOT have more than 3 items. Value: [1,\\"test\\",2,2]. + - \\"rules['b']\\" should match exactly one schema in oneOf. Value: [1,\\"test\\",2,2]. +" +`; + +exports[`validation should fail for rules3 1`] = ` +"Commitlint configuration in rules3.js is invalid: + - \\"rules['c']\\" should NOT have fewer than 1 items. Value: []. + - \\"rules['c']\\" should match exactly one schema in oneOf. Value: []. +" +`; + +exports[`validation should fail for rules4 1`] = ` +"Commitlint configuration in rules4.js is invalid: + - Property \\"rules['d'][0]\\" has the wrong type - should be number. + - \\"rules['d'][0]\\" should be equal to one of the allowed values. Value: []. + - \\"rules['d']\\" should match exactly one schema in oneOf. Value: [[],[],[]]. +" +`; + +exports[`validation should fail for rules5 1`] = ` +"Commitlint configuration in rules5.js is invalid: + - Property \\"rules['e']\\" has the wrong type - should be array. + - \\"rules['e']\\" should match exactly one schema in oneOf. Value: {}. +" +`; + +exports[`validation should fail for rulesAsArray 1`] = ` +"Commitlint configuration in rulesAsArray.js is invalid: + - Property \\"rules\\" has the wrong type - should be object. +" +`; + +exports[`validation should fail for whenConfigIsNotObject 1`] = ` +"Commitlint configuration in whenConfigIsNotObject.js is invalid: + - Config has the wrong type - should be object. +" +`; + +exports[`validation should fail for whenConfigIsNotObject2 1`] = ` +"Commitlint configuration in whenConfigIsNotObject2.js is invalid: + - Config has the wrong type - should be object. +" +`; + +exports[`validation should fail for withPluginsAsObject 1`] = ` +"Commitlint configuration in withPluginsAsObject.js is invalid: + - Property \\"plugins[0]\\" has the wrong type - should be string. + - \\"plugins[0]\\" should have required property '.rules'. Value: {}. + - \\"plugins[0]\\" should match some schema in anyOf. Value: {}. +" +`; diff --git a/@commitlint/config-validator/src/commitlint.schema.json b/@commitlint/config-validator/src/commitlint.schema.json new file mode 100644 index 0000000000..0e03f5f211 --- /dev/null +++ b/@commitlint/config-validator/src/commitlint.schema.json @@ -0,0 +1,96 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "default": {}, + "type": "object", + "definitions": { + "rule": { + "oneOf": [ + { + "description": "A rule", + "type": "array", + "items": [ + { + "description": "Level: 0 disables the rule. For 1 it will be considered a warning, for 2 an error", + "type": "number", + "enum": [0, 1, 2] + }, + { + "description": "Applicable: always|never: never inverts the rule", + "type": "string", + "enum": ["always", "never"] + }, + { + "description": "Value: the value for this rule" + } + ], + "minItems": 1, + "maxItems": 3, + "additionalItems": false + } + ] + } + }, + "properties": { + "extends": { + "description": "Resolveable ids to commitlint configurations to extend", + "oneOf": [ + { + "type": "array", + "items": {"type": "string"} + }, + {"type": "string"} + ] + }, + "parserPreset": { + "description": "Resolveable id to conventional-changelog parser preset to import and use", + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "name": {"type": "string"}, + "path": {"type": "string"}, + "parserOpts": {} + }, + "additionalProperties": true + } + ] + }, + "helpUrl": { + "description": "Custom URL to show upon failure", + "type": "string" + }, + "formatter": { + "description": "Resolveable id to package, from node_modules, which formats the output", + "type": "string" + }, + "rules": { + "description": "Rules to check against", + "type": "object", + "propertyNames": {"type": "string"}, + "additionalProperties": {"$ref": "#/definitions/rule"} + }, + "plugins": { + "description": "Resolveable ids of commitlint plugins from node_modules", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + { + "required": ["rules"], + "rules": {} + } + ] + } + }, + "ignores": { + "type": "array", + "items": {"typeof": "function"}, + "description": "Additional commits to ignore, defined by ignore matchers" + }, + "defaultIgnores": { + "description": "Whether commitlint uses the default ignore rules", + "type": "boolean" + } + } +} diff --git a/@commitlint/config-validator/src/formatErrors.ts b/@commitlint/config-validator/src/formatErrors.ts new file mode 100644 index 0000000000..ef1fbacc4b --- /dev/null +++ b/@commitlint/config-validator/src/formatErrors.ts @@ -0,0 +1,45 @@ +import {ErrorObject} from 'ajv'; + +/** + * Formats an array of schema validation errors. + * @param errors An array of error messages to format. + * @returns Formatted error message + * Based on https://github.com/eslint/eslint/blob/master/lib/shared/config-validator.js#L237-L261 + */ +export function formatErrors(errors: ErrorObject[]): string { + return errors + .map((error) => { + if ( + error.keyword === 'additionalProperties' && + 'additionalProperty' in error.params + ) { + const formattedPropertyPath = error.dataPath.length + ? `${error.dataPath.slice(1)}.${error.params.additionalProperty}` + : error.params.additionalProperty; + + return `Unexpected top-level property "${formattedPropertyPath}"`; + } + if (error.keyword === 'type') { + const formattedField = error.dataPath.slice(1); + if (!formattedField) { + return `Config has the wrong type - ${error.message}`; + } + return `Property "${formattedField}" has the wrong type - ${error.message}`; + } + const field = + (error.dataPath[0] === '.' + ? error.dataPath.slice(1) + : error.dataPath) || 'Config'; + if (error.keyword === 'typeof') { + return `"${field}" should be a ${error.schema}. Value: ${JSON.stringify( + error.data + )}`; + } + + return `"${field}" ${error.message}. Value: ${JSON.stringify( + error.data + )}`; + }) + .map((message) => `\t- ${message}.\n`) + .join(''); +} diff --git a/@commitlint/config-validator/src/validate.test.ts b/@commitlint/config-validator/src/validate.test.ts new file mode 100644 index 0000000000..5f183476c2 --- /dev/null +++ b/@commitlint/config-validator/src/validate.test.ts @@ -0,0 +1,67 @@ +import {validateConfig} from './validate'; +import {UserConfig} from '@commitlint/types'; + +const validSchemas: Record = { + empty: {}, + withEmptyExtends: {extends: []}, + withStringExtends: {extends: 'test'}, + withSingleExtends: {extends: ['test']}, + withMultipleExtends: {extends: ['test', 'test2']}, + withFormatter: {formatter: ''}, + withHelpUrl: {helpUrl: ''}, + withRules: {rules: {a: [0], b: [1, 'never'], c: [2, 'never', true]}}, + withParserPresetString: {parserPreset: 'test'}, + withParserPresetObject: {parserPreset: {}}, + withParserPresetObject2: {parserPreset: {name: 'string', path: 'string'}}, + withParserPresetObjectPromise: { + parserPreset: Promise.resolve({name: 'string'}), + }, + withParserPresetOpts: {parserPreset: {parserOpts: {test: 1}}}, + withParserPresetOptsPromise: { + parserPreset: {parserOpts: Promise.resolve({test: 1})}, + }, + withEmptyIgnores: {ignores: []}, + withIgnores: {ignores: [() => true]}, + withDefaultIgnoresTrue: {defaultIgnores: true}, + withDefaultIgnoresFalse: {defaultIgnores: false}, + withEmptyPlugins: {plugins: []}, + withPluginsAsString: {plugins: ['test']}, + withPluginsAsObject: {plugins: [{rules: {}}]}, + shouldSkipAllowAdditionalProperties: {foo: 1}, +}; + +const invalidSchemas: Record = { + whenConfigIsNotObject: [], + whenConfigIsNotObject2: '', + extendsAsObject: {extends: {test: 1}}, + extendsWithFunction: {extends: [() => true]}, + formatterAsObject: {formatter: {}}, + helpUrlAsArray: {helpUrl: []}, + rulesAsArray: {rules: ['a']}, + rules1: {rules: {a: [3]}}, + rules2: {rules: {b: [1, 'test', 2, 2]}}, + rules3: {rules: {c: []}}, + rules4: {rules: {d: [[], [], []]}}, + rules5: {rules: {e: {}}}, + parserPreset: {parserPreset: []}, + ignoresFunction: {ignores: () => true}, + ignoresNotFunction: {ignores: [1]}, + defaultIgnoresNotBoolean: {defaultIgnores: 'true'}, + pluginsNotArray: {plugins: 'test'}, + withPluginsAsObject: {plugins: [{}]}, + helpUrlNotString: {helpUrl: {}}, +}; + +describe('validation should pass for', () => { + test.each(Object.entries(validSchemas))('%s', (file, config) => { + expect(() => validateConfig(`${file}.js`, config)).not.toThrowError(); + }); +}); + +describe('validation should fail for', () => { + test.each(Object.entries(invalidSchemas))('%s', (file, config) => { + expect(() => + validateConfig(`${file}.js`, config) + ).toThrowErrorMatchingSnapshot(); + }); +}); diff --git a/@commitlint/config-validator/src/validate.ts b/@commitlint/config-validator/src/validate.ts new file mode 100644 index 0000000000..ac87f3b8c4 --- /dev/null +++ b/@commitlint/config-validator/src/validate.ts @@ -0,0 +1,47 @@ +import Ajv from 'ajv'; +import {UserConfig} from '@commitlint/types'; +import schema from './commitlint.schema.json'; +import {formatErrors} from './formatErrors'; + +const TYPE_OF = [ + 'undefined', + 'string', + 'number', + 'object', + 'function', + 'boolean', + 'symbol', +]; + +export function validateConfig( + source: string, + config: unknown +): asserts config is UserConfig { + const ajv = new Ajv({ + meta: false, + useDefaults: true, + validateSchema: false, + missingRefs: 'ignore', + verbose: true, + schemaId: 'auto', + }); + + ajv.addKeyword('typeof', { + validate: function typeOfFunc(schema: any, data: any) { + return typeof data === schema; + }, + metaSchema: {type: 'string', enum: TYPE_OF}, + schema: true, + }); + + const validate = ajv.compile(schema); + const isValid = validate(config); + + if (!isValid && validate.errors && validate.errors.length) { + throw new Error( + `Commitlint configuration in ${source} is invalid:\n${formatErrors( + validate.errors + )}` + ); + } +} diff --git a/@commitlint/config-validator/tsconfig.json b/@commitlint/config-validator/tsconfig.json new file mode 100644 index 0000000000..65f2e6c397 --- /dev/null +++ b/@commitlint/config-validator/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.shared.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["./src/**/*.ts", "./src/**/*.json"], + "exclude": ["./src/**/*.test.ts", "./lib/**/*"], + "references": [{"path": "../types"}] +} diff --git a/@commitlint/load/fixtures/outer-scope/commitlint.config.js b/@commitlint/load/fixtures/outer-scope/commitlint.config.js index 23554f588f..9b27c320a7 100644 --- a/@commitlint/load/fixtures/outer-scope/commitlint.config.js +++ b/@commitlint/load/fixtures/outer-scope/commitlint.config.js @@ -1,7 +1,7 @@ module.exports = { rules: { - outer: true, - inner: false, - child: false - } + outer: [1, 'never', true], + inner: [1, 'never', false], + child: [1, 'never', false], + }, }; diff --git a/@commitlint/load/fixtures/outer-scope/inner-scope/child-scope/commitlint.config.js b/@commitlint/load/fixtures/outer-scope/inner-scope/child-scope/commitlint.config.js index 925d251382..d2c04c8054 100644 --- a/@commitlint/load/fixtures/outer-scope/inner-scope/child-scope/commitlint.config.js +++ b/@commitlint/load/fixtures/outer-scope/inner-scope/child-scope/commitlint.config.js @@ -1,7 +1,7 @@ module.exports = { rules: { - outer: false, - inner: false, - child: true - } + outer: [2, 'always', false], + inner: [2, 'always', false], + child: [2, 'always', true], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-js/.commitlintrc.js b/@commitlint/load/fixtures/recursive-extends-js/.commitlintrc.js index 7564fdc432..f90e771292 100644 --- a/@commitlint/load/fixtures/recursive-extends-js/.commitlintrc.js +++ b/@commitlint/load/fixtures/recursive-extends-js/.commitlintrc.js @@ -1,6 +1,6 @@ module.exports = { extends: ['./first-extended'], rules: { - zero: 0 - } + zero: [0, 'never'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-js/first-extended/index.js b/@commitlint/load/fixtures/recursive-extends-js/first-extended/index.js index 4317428ad1..d26b0ff209 100644 --- a/@commitlint/load/fixtures/recursive-extends-js/first-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends-js/first-extended/index.js @@ -1,6 +1,6 @@ module.exports = { extends: ['./second-extended'], rules: { - one: 1 - } + one: [1, 'never'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-js/first-extended/second-extended/index.js b/@commitlint/load/fixtures/recursive-extends-js/first-extended/second-extended/index.js index d199d354da..64caae544a 100644 --- a/@commitlint/load/fixtures/recursive-extends-js/first-extended/second-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends-js/first-extended/second-extended/index.js @@ -1,5 +1,5 @@ module.exports = { rules: { - two: 2 - } + two: [2, 'always'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-json/.commitlintrc.json b/@commitlint/load/fixtures/recursive-extends-json/.commitlintrc.json index 9a5e8e1d9f..97335f47a5 100644 --- a/@commitlint/load/fixtures/recursive-extends-json/.commitlintrc.json +++ b/@commitlint/load/fixtures/recursive-extends-json/.commitlintrc.json @@ -1,6 +1,6 @@ { "extends": ["./first-extended"], "rules": { - "zero": 0 + "zero": [0, "never"] } } diff --git a/@commitlint/load/fixtures/recursive-extends-json/first-extended/index.js b/@commitlint/load/fixtures/recursive-extends-json/first-extended/index.js index 4317428ad1..8bc1854ccc 100644 --- a/@commitlint/load/fixtures/recursive-extends-json/first-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends-json/first-extended/index.js @@ -1,6 +1,6 @@ module.exports = { extends: ['./second-extended'], rules: { - one: 1 - } + one: [1, 'always'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-json/first-extended/second-extended/index.js b/@commitlint/load/fixtures/recursive-extends-json/first-extended/second-extended/index.js index d199d354da..b0902ace44 100644 --- a/@commitlint/load/fixtures/recursive-extends-json/first-extended/second-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends-json/first-extended/second-extended/index.js @@ -1,5 +1,5 @@ module.exports = { rules: { - two: 2 - } + two: [2, 'never'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-package/first-extended/index.js b/@commitlint/load/fixtures/recursive-extends-package/first-extended/index.js index 4317428ad1..d26b0ff209 100644 --- a/@commitlint/load/fixtures/recursive-extends-package/first-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends-package/first-extended/index.js @@ -1,6 +1,6 @@ module.exports = { extends: ['./second-extended'], rules: { - one: 1 - } + one: [1, 'never'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-package/first-extended/second-extended/index.js b/@commitlint/load/fixtures/recursive-extends-package/first-extended/second-extended/index.js index d199d354da..b0902ace44 100644 --- a/@commitlint/load/fixtures/recursive-extends-package/first-extended/second-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends-package/first-extended/second-extended/index.js @@ -1,5 +1,5 @@ module.exports = { rules: { - two: 2 - } + two: [2, 'never'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-package/package.json b/@commitlint/load/fixtures/recursive-extends-package/package.json index 1818049244..704cc882f1 100644 --- a/@commitlint/load/fixtures/recursive-extends-package/package.json +++ b/@commitlint/load/fixtures/recursive-extends-package/package.json @@ -4,7 +4,10 @@ "./first-extended" ], "rules": { - "zero": 0 + "zero": [ + 0, + "never" + ] } } } diff --git a/@commitlint/load/fixtures/recursive-extends-yaml/.commitlintrc.yml b/@commitlint/load/fixtures/recursive-extends-yaml/.commitlintrc.yml index a5ef7fdffd..c7c751a959 100644 --- a/@commitlint/load/fixtures/recursive-extends-yaml/.commitlintrc.yml +++ b/@commitlint/load/fixtures/recursive-extends-yaml/.commitlintrc.yml @@ -1,4 +1,4 @@ extends: - './first-extended' rules: - zero: 0 + zero: [0, 'never'] diff --git a/@commitlint/load/fixtures/recursive-extends-yaml/first-extended/index.js b/@commitlint/load/fixtures/recursive-extends-yaml/first-extended/index.js index 4317428ad1..d26b0ff209 100644 --- a/@commitlint/load/fixtures/recursive-extends-yaml/first-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends-yaml/first-extended/index.js @@ -1,6 +1,6 @@ module.exports = { extends: ['./second-extended'], rules: { - one: 1 - } + one: [1, 'never'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-yaml/first-extended/second-extended/index.js b/@commitlint/load/fixtures/recursive-extends-yaml/first-extended/second-extended/index.js index d199d354da..64caae544a 100644 --- a/@commitlint/load/fixtures/recursive-extends-yaml/first-extended/second-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends-yaml/first-extended/second-extended/index.js @@ -1,5 +1,5 @@ module.exports = { rules: { - two: 2 - } + two: [2, 'always'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends/commitlint.config.js b/@commitlint/load/fixtures/recursive-extends/commitlint.config.js index 7564fdc432..f90e771292 100644 --- a/@commitlint/load/fixtures/recursive-extends/commitlint.config.js +++ b/@commitlint/load/fixtures/recursive-extends/commitlint.config.js @@ -1,6 +1,6 @@ module.exports = { extends: ['./first-extended'], rules: { - zero: 0 - } + zero: [0, 'never'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends/first-extended/commitlint.config.js b/@commitlint/load/fixtures/recursive-extends/first-extended/commitlint.config.js index 4317428ad1..8bc1854ccc 100644 --- a/@commitlint/load/fixtures/recursive-extends/first-extended/commitlint.config.js +++ b/@commitlint/load/fixtures/recursive-extends/first-extended/commitlint.config.js @@ -1,6 +1,6 @@ module.exports = { extends: ['./second-extended'], rules: { - one: 1 - } + one: [1, 'always'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends/first-extended/second-extended/index.js b/@commitlint/load/fixtures/recursive-extends/first-extended/second-extended/index.js index d199d354da..b0902ace44 100644 --- a/@commitlint/load/fixtures/recursive-extends/first-extended/second-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends/first-extended/second-extended/index.js @@ -1,5 +1,5 @@ module.exports = { rules: { - two: 2 - } + two: [2, 'never'], + }, }; diff --git a/@commitlint/load/fixtures/specify-config-file/commitlint.config.js b/@commitlint/load/fixtures/specify-config-file/commitlint.config.js index 1fd91e7bb1..31855af125 100644 --- a/@commitlint/load/fixtures/specify-config-file/commitlint.config.js +++ b/@commitlint/load/fixtures/specify-config-file/commitlint.config.js @@ -1,6 +1,6 @@ module.exports = { rules: { - foo: 'hello', - bar: 'world' - } + foo: [1, 'never', 'hello'], + bar: [1, 'never', 'world'], + }, }; diff --git a/@commitlint/load/fixtures/specify-config-file/config/commitlint.config.js b/@commitlint/load/fixtures/specify-config-file/config/commitlint.config.js index be422cfda5..fbf4c8adc9 100644 --- a/@commitlint/load/fixtures/specify-config-file/config/commitlint.config.js +++ b/@commitlint/load/fixtures/specify-config-file/config/commitlint.config.js @@ -1,5 +1,5 @@ module.exports = { rules: { - foo: 'bar' - } + foo: [1, 'always', 'bar'], + }, }; diff --git a/@commitlint/load/fixtures/trash-extend/commitlint.config.js b/@commitlint/load/fixtures/trash-extend/commitlint.config.js index 3b41dace2b..7986e40f48 100644 --- a/@commitlint/load/fixtures/trash-extend/commitlint.config.js +++ b/@commitlint/load/fixtures/trash-extend/commitlint.config.js @@ -2,6 +2,6 @@ module.exports = { extends: ['./one'], zero: '0', rules: { - zero: 0 - } + zero: [0, 'always', 'zero'], + }, }; diff --git a/@commitlint/load/fixtures/trash-extend/one.js b/@commitlint/load/fixtures/trash-extend/one.js index 60f3a3530d..ae7db74d52 100644 --- a/@commitlint/load/fixtures/trash-extend/one.js +++ b/@commitlint/load/fixtures/trash-extend/one.js @@ -1,6 +1,6 @@ module.exports = { one: 1, rules: { - one: 1 - } + one: [1, 'always', 'one'], + }, }; diff --git a/@commitlint/load/fixtures/trash-file/commitlint.config.js b/@commitlint/load/fixtures/trash-file/commitlint.config.js index a7a8e43bd8..477faba44e 100644 --- a/@commitlint/load/fixtures/trash-file/commitlint.config.js +++ b/@commitlint/load/fixtures/trash-file/commitlint.config.js @@ -2,7 +2,7 @@ module.exports = { foo: 'bar', baz: 'bar', rules: { - foo: 'bar', - baz: 'bar' - } + foo: [1, 'always', 'bar'], + baz: [1, 'always', 'bar'], + }, }; diff --git a/@commitlint/load/package.json b/@commitlint/load/package.json index bbf2cf9aee..47f6b9a108 100644 --- a/@commitlint/load/package.json +++ b/@commitlint/load/package.json @@ -42,6 +42,7 @@ "dependencies": { "@commitlint/execute-rule": "^11.0.0", "@commitlint/resolve-extends": "^11.0.0", + "@commitlint/config-validator": "^11.0.0", "@commitlint/types": "^11.0.0", "chalk": "^4.0.0", "cosmiconfig": "^7.0.0", diff --git a/@commitlint/load/src/load.test.ts b/@commitlint/load/src/load.test.ts index ff3530a0c1..22cd2b2cbf 100644 --- a/@commitlint/load/src/load.test.ts +++ b/@commitlint/load/src/load.test.ts @@ -165,8 +165,8 @@ test('respects cwd option', async () => { extends: ['./second-extended'], plugins: {}, rules: { - one: 1, - two: 2, + one: [1, 'always'], + two: [2, 'never'], }, }); }); @@ -180,9 +180,9 @@ test('recursive extends', async () => { extends: ['./first-extended'], plugins: {}, rules: { - zero: 0, - one: 1, - two: 2, + zero: [0, 'never'], + one: [1, 'always'], + two: [2, 'never'], }, }); }); @@ -196,9 +196,9 @@ test('recursive extends with json file', async () => { extends: ['./first-extended'], plugins: {}, rules: { - zero: 0, - one: 1, - two: 2, + zero: [0, 'never'], + one: [1, 'always'], + two: [2, 'never'], }, }); }); @@ -212,9 +212,9 @@ test('recursive extends with yaml file', async () => { extends: ['./first-extended'], plugins: {}, rules: { - zero: 0, - one: 1, - two: 2, + zero: [0, 'never'], + one: [1, 'never'], + two: [2, 'always'], }, }); }); @@ -228,9 +228,9 @@ test('recursive extends with js file', async () => { extends: ['./first-extended'], plugins: {}, rules: { - zero: 0, - one: 1, - two: 2, + zero: [0, 'never'], + one: [1, 'never'], + two: [2, 'always'], }, }); }); @@ -244,9 +244,9 @@ test('recursive extends with package.json file', async () => { extends: ['./first-extended'], plugins: {}, rules: { - zero: 0, - one: 1, - two: 2, + zero: [0, 'never'], + one: [1, 'never'], + two: [2, 'never'], }, }); }); @@ -273,7 +273,7 @@ test('recursive extends with parserPreset', async () => { }); }); -test('ignores unknow keys', async () => { +test('ignores unknown keys', async () => { const cwd = await gitBootstrap('fixtures/trash-file'); const actual = await load({}, {cwd}); @@ -282,13 +282,13 @@ test('ignores unknow keys', async () => { extends: [], plugins: {}, rules: { - foo: 'bar', - baz: 'bar', + foo: [1, 'always', 'bar'], + baz: [1, 'always', 'bar'], }, }); }); -test('ignores unknow keys recursively', async () => { +test('ignores unknown keys recursively', async () => { const cwd = await gitBootstrap('fixtures/trash-extend'); const actual = await load({}, {cwd}); @@ -297,8 +297,8 @@ test('ignores unknow keys recursively', async () => { extends: ['./one'], plugins: {}, rules: { - zero: 0, - one: 1, + zero: [0, 'always', 'zero'], + one: [1, 'always', 'one'], }, }); }); @@ -314,9 +314,9 @@ test('find up from given cwd', async () => { extends: [], plugins: {}, rules: { - child: true, - inner: false, - outer: false, + child: [2, 'always', true], + inner: [2, 'always', false], + outer: [2, 'always', false], }, }); }); @@ -331,9 +331,9 @@ test('find up config from outside current git repo', async () => { extends: [], plugins: {}, rules: { - child: false, - inner: false, - outer: true, + child: [1, 'never', false], + inner: [1, 'never', false], + outer: [1, 'never', true], }, }); }); diff --git a/@commitlint/load/src/load.ts b/@commitlint/load/src/load.ts index 4ff22804b4..eef79ceb5e 100644 --- a/@commitlint/load/src/load.ts +++ b/@commitlint/load/src/load.ts @@ -6,6 +6,7 @@ import resolveFrom from 'resolve-from'; import executeRule from '@commitlint/execute-rule'; import resolveExtends from '@commitlint/resolve-extends'; +import {validateConfig} from '@commitlint/config-validator'; import { UserConfig, LoadOptions, @@ -17,7 +18,6 @@ import { import loadPlugin from './utils/load-plugin'; import {loadConfig} from './utils/load-config'; import {loadParserOpts} from './utils/load-parser-opts'; -import {pickConfig} from './utils/pick-config'; export default async function load( seed: UserConfig = {}, @@ -26,21 +26,21 @@ export default async function load( const cwd = typeof options.cwd === 'undefined' ? process.cwd() : options.cwd; const loaded = await loadConfig(cwd, options.file); const base = loaded && loaded.filepath ? Path.dirname(loaded.filepath) : cwd; - - // TODO: validate loaded.config against UserConfig type - // Might amount to breaking changes, defer until 9.0.0 + let config: UserConfig = {}; + if (loaded) { + validateConfig(loaded.filepath || '', loaded.config); + config = loaded.config; + } // Merge passed config with file based options - const config = pickConfig( - merge( - { - extends: [], - plugins: [], - rules: {}, - }, - loaded ? loaded.config : null, - seed - ) + config = merge( + { + extends: [], + plugins: [], + rules: {}, + }, + config, + seed ); // Resolve parserPreset key @@ -55,17 +55,17 @@ export default async function load( } // Resolve extends key - const extended = (resolveExtends(config, { + const extended = resolveExtends(config, { prefix: 'commitlint-config', cwd: base, parserPreset: config.parserPreset, - }) as unknown) as UserConfig; + }); - if (!extended.formatter || typeof extended.formatter !== 'string') { + if (!extended.formatter) { extended.formatter = '@commitlint/format'; } - if (!extended.helpUrl || typeof extended.helpUrl !== 'string') { + if (!extended.helpUrl) { extended.helpUrl = 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint'; } diff --git a/@commitlint/load/src/utils/pick-config.ts b/@commitlint/load/src/utils/pick-config.ts deleted file mode 100644 index 757ed0f702..0000000000 --- a/@commitlint/load/src/utils/pick-config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import pick from 'lodash/pick'; - -export const pickConfig = (input: unknown): Record => - pick( - input, - 'extends', - 'rules', - 'plugins', - 'parserPreset', - 'formatter', - 'ignores', - 'defaultIgnores', - 'helpUrl' - ); diff --git a/@commitlint/load/tsconfig.json b/@commitlint/load/tsconfig.json index 51cdcedab0..1b645658c1 100644 --- a/@commitlint/load/tsconfig.json +++ b/@commitlint/load/tsconfig.json @@ -10,6 +10,7 @@ "references": [ {"path": "../execute-rule"}, {"path": "../resolve-extends"}, + {"path": "../config-validator"}, {"path": "../types"} ] } diff --git a/@commitlint/resolve-extends/package.json b/@commitlint/resolve-extends/package.json index 8d69557250..43aef93ab3 100644 --- a/@commitlint/resolve-extends/package.json +++ b/@commitlint/resolve-extends/package.json @@ -38,6 +38,8 @@ "@types/lodash": "^4.14.161" }, "dependencies": { + "@commitlint/config-validator": "^11.0.0", + "@commitlint/types": "^11.0.0", "import-fresh": "^3.0.0", "lodash": "^4.17.19", "resolve-from": "^5.0.0", diff --git a/@commitlint/resolve-extends/src/index.test.ts b/@commitlint/resolve-extends/src/index.test.ts index 98099af3dd..87c00f82b9 100644 --- a/@commitlint/resolve-extends/src/index.test.ts +++ b/@commitlint/resolve-extends/src/index.test.ts @@ -1,4 +1,5 @@ import resolveExtends, {ResolveExtendsContext} from '.'; +import {UserConfig} from '@commitlint/types'; const id = (id: unknown) => id; @@ -213,17 +214,17 @@ test('propagates contents recursively', () => { }); test('propagates contents recursively with overlap', () => { - const input = {extends: ['extender-name']}; + const input: UserConfig = {extends: ['extender-name']}; - const require = (id: string) => { + const require = (id: string): UserConfig => { switch (id) { case 'extender-name': return { extends: ['recursive-extender-name'], - rules: {rule: ['zero', 'one']}, + rules: {rule: [1, 'always']}, }; case 'recursive-extender-name': - return {rules: {rule: ['two', 'three', 'four']}}; + return {rules: {rule: [2, 'never', 'four']}}; default: return {}; } @@ -233,10 +234,10 @@ test('propagates contents recursively with overlap', () => { const actual = resolveExtends(input, ctx); - const expected = { + const expected: UserConfig = { extends: ['extender-name'], rules: { - rule: ['zero', 'one'], + rule: [1, 'always'], }, }; @@ -244,14 +245,14 @@ test('propagates contents recursively with overlap', () => { }); test('extends rules from left to right with overlap', () => { - const input = {extends: ['left', 'right']}; + const input: UserConfig = {extends: ['left', 'right']}; - const require = (id: string) => { + const require = (id: string): UserConfig => { switch (id) { case 'left': - return {rules: {a: true}}; + return {rules: {a: [0, 'never', true]}}; case 'right': - return {rules: {a: false, b: true}}; + return {rules: {a: [0, 'never', false], b: [0, 'never', true]}}; default: return {}; } @@ -261,11 +262,11 @@ test('extends rules from left to right with overlap', () => { const actual = resolveExtends(input, ctx); - const expected = { + const expected: UserConfig = { extends: ['left', 'right'], rules: { - a: false, - b: true, + a: [0, 'never', false], + b: [0, 'never', true], }, }; @@ -378,22 +379,25 @@ test('plugins should be merged correctly', () => { }); test('rules should be merged correctly', () => { - const input = {extends: ['extender-name'], rules: {test1: ['base', '1']}}; + const input: UserConfig = { + extends: ['extender-name'], + rules: {test1: [1, 'never', 'base']}, + }; - const require = (id: string) => { + const require = (id: string): UserConfig => { switch (id) { case 'extender-name': return { extends: ['recursive-extender-name'], - rules: {test2: [id, '2']}, + rules: {test2: [2, 'never', id]}, }; case 'recursive-extender-name': return { extends: ['second-recursive-extender-name'], - rules: {test1: [id, '3']}, + rules: {test1: [0, 'never', id]}, }; case 'second-recursive-extender-name': - return {rules: {test2: [id, '4']}}; + return {rules: {test2: [1, 'never', id]}}; default: return {}; } @@ -403,11 +407,11 @@ test('rules should be merged correctly', () => { const actual = resolveExtends(input, ctx); - const expected = { + const expected: UserConfig = { extends: ['extender-name'], rules: { - test1: ['base', '1'], - test2: ['extender-name', '2'], + test1: [1, 'never', 'base'], + test2: [2, 'never', 'extender-name'], }, }; diff --git a/@commitlint/resolve-extends/src/index.ts b/@commitlint/resolve-extends/src/index.ts index 597a9db556..d9f2ba6758 100644 --- a/@commitlint/resolve-extends/src/index.ts +++ b/@commitlint/resolve-extends/src/index.ts @@ -2,21 +2,12 @@ import path from 'path'; import 'resolve-global'; import resolveFrom from 'resolve-from'; -import merge from 'lodash/merge'; import mergeWith from 'lodash/mergeWith'; +import {validateConfig} from '@commitlint/config-validator'; +import {UserConfig} from '@commitlint/types'; const importFresh = require('import-fresh'); -export interface ResolvedConfig { - parserPreset?: unknown; - [key: string]: unknown; -} - -export interface ResolveExtendsConfig { - extends?: string | string[]; - [key: string]: unknown; -} - export interface ResolveExtendsContext { cwd?: string; parserPreset?: unknown; @@ -27,9 +18,9 @@ export interface ResolveExtendsContext { } export default function resolveExtends( - config: ResolveExtendsConfig = {}, + config: UserConfig = {}, context: ResolveExtendsContext = {} -): ResolvedConfig { +): UserConfig { const {extends: e} = config; const extended = loadExtends(config, context); extended.push(config); @@ -49,18 +40,18 @@ export default function resolveExtends( } function loadExtends( - config: ResolveExtendsConfig = {}, + config: UserConfig = {}, context: ResolveExtendsContext = {} -): ResolvedConfig[] { +): UserConfig[] { const {extends: e} = config; const ext = e ? (Array.isArray(e) ? e : [e]) : []; - return ext.reduceRight((configs, raw) => { + return ext.reduceRight((configs, raw) => { const load = context.require || require; const resolved = resolveConfig(raw, context); const c = load(resolved); const cwd = path.dirname(resolved); - const ctx = merge({}, context, {cwd}); + const ctx = {...context, cwd}; // Resolve parser preset if none was present before if ( @@ -81,6 +72,8 @@ function loadExtends( config.parserPreset = parserPreset; } + validateConfig(resolved, config); + return [...loadExtends(c, ctx), c, ...configs]; }, []); } diff --git a/@commitlint/resolve-extends/tsconfig.json b/@commitlint/resolve-extends/tsconfig.json index 49479bf34f..d424776e00 100644 --- a/@commitlint/resolve-extends/tsconfig.json +++ b/@commitlint/resolve-extends/tsconfig.json @@ -2,9 +2,11 @@ "extends": "../../tsconfig.shared.json", "compilerOptions": { "composite": true, + "isolatedModules": false, "rootDir": "./src", "outDir": "./lib" }, "include": ["./src"], - "exclude": ["./src/**/*.test.ts", "./lib/**/*"] + "exclude": ["./src/**/*.test.ts", "./lib/**/*"], + "references": [{"path": "../types"}, {"path": "../config-validator"}] } diff --git a/@commitlint/types/src/load.ts b/@commitlint/types/src/load.ts index c24a81bfb9..546a3ca70f 100644 --- a/@commitlint/types/src/load.ts +++ b/@commitlint/types/src/load.ts @@ -22,16 +22,7 @@ export interface UserConfig { defaultIgnores?: boolean; plugins?: (string | Plugin)[]; helpUrl?: string; -} - -export interface UserPreset { - extends?: string[]; - formatter?: string; - rules?: Partial; - parserPreset?: string | ParserPreset; - ignores?: ((commit: string) => boolean)[]; - defaultIgnores?: boolean; - plugins: PluginRecords; + [key: string]: unknown; } export type QualifiedRules = Partial>; diff --git a/tsconfig.json b/tsconfig.json index 4dec545ae4..4af014cdaa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "references": [ {"path": "@packages/test-environment"}, {"path": "@packages/test"}, + {"path": "@commitlint/config-validator"}, {"path": "@commitlint/ensure"}, {"path": "@commitlint/execute-rule"}, {"path": "@commitlint/format"}, diff --git a/tsconfig.shared.json b/tsconfig.shared.json index ad1bb5892d..387485948c 100644 --- a/tsconfig.shared.json +++ b/tsconfig.shared.json @@ -7,6 +7,7 @@ "sourceMap": true, "module": "commonjs", "esModuleInterop": true, + "resolveJsonModule": true, "allowSyntheticDefaultImports": true, "strict": true, "allowUnreachableCode": false, diff --git a/yarn.lock b/yarn.lock index cd53ccffc3..c2f360bb43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2360,7 +2360,7 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -ajv@^6.10.0, ajv@^6.12.4: +ajv@^6.5.5, ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.6: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2370,26 +2370,6 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^6.12.3: - version "6.12.5" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" - integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^6.5.5: - version "6.11.0" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz#c3607cbc8ae392d8a5a536f25b21f8e5f3f87fe9" - integrity sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - ajv@^7.0.2: version "7.0.2" resolved "https://registry.npmjs.org/ajv/-/ajv-7.0.2.tgz#04ccc89a93449c64382fe0846d45a7135d986dbc"