diff --git a/.editorconfig b/.editorconfig index 62fcd43244..0186fe53aa 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,7 +4,7 @@ insert_final_newline = true trim_trailing_whitespace = true indent_style = tab -[{.*rc,*.yml,*.md,package.json,lerna.json,*.svg}] +[{.*rc,*.yml,*.md,package.json,lerna.json,tsconfig.json,tsconfig.*.json,*.svg}] indent_style = space [*.md] diff --git a/@commitlint/ensure/src/index.ts b/@commitlint/ensure/src/index.ts index 143d6d078c..7ff21e7e19 100644 --- a/@commitlint/ensure/src/index.ts +++ b/@commitlint/ensure/src/index.ts @@ -1,4 +1,4 @@ -import ensureCase from './case'; +import ensureCase, {TargetCaseType} from './case'; import ensureEnum from './enum'; import maxLength from './max-length'; import maxLineLength from './max-line-length'; @@ -7,5 +7,5 @@ import notEmpty from './not-empty'; export {ensureCase as case}; export {ensureEnum as enum}; +export {TargetCaseType}; export {maxLength, maxLineLength, minLength, notEmpty}; -export {TargetCaseType} from './case'; diff --git a/@commitlint/execute-rule/src/index.ts b/@commitlint/execute-rule/src/index.ts index c0db79c7f0..47029f4b89 100644 --- a/@commitlint/execute-rule/src/index.ts +++ b/@commitlint/execute-rule/src/index.ts @@ -7,7 +7,7 @@ type ExecutedRule = readonly [string, T]; export default execute; export async function execute( - rule: Rule + rule?: Rule ): Promise | null> { if (!Array.isArray(rule)) { return null; diff --git a/@commitlint/load/fixtures/basic/commitlint.config.js b/@commitlint/load/fixtures/basic/commitlint.config.js index 9644c8e61f..9c5695d7aa 100644 --- a/@commitlint/load/fixtures/basic/commitlint.config.js +++ b/@commitlint/load/fixtures/basic/commitlint.config.js @@ -1,5 +1,5 @@ module.exports = { rules: { - basic: true + 'body-case': [1, 'never', 'camel-case'] } }; diff --git a/@commitlint/load/package.json b/@commitlint/load/package.json index 0d8cbddadf..906814f897 100644 --- a/@commitlint/load/package.json +++ b/@commitlint/load/package.json @@ -2,42 +2,14 @@ "name": "@commitlint/load", "version": "8.3.5", "description": "Load shared commitlint configuration", - "main": "lib/index.js", + "main": "lib/load.js", + "types": "lib/load.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": "concurrently \"ava -c 4 --verbose --watch\" \"yarn run watch\"", - "test": "ava -c 4 --verbose && ava src/index.serial-test.js --verbose", - "watch": "babel src --out-dir lib --watch --source-maps" - }, - "ava": { - "files": [ - "src/**/*.test.js", - "!lib/**/*" - ], - "source": [ - "src/**/*.js", - "!lib/**/*" - ], - "babel": { - "testOptions": { - "presets": [ - "babel-preset-commitlint" - ] - } - }, - "require": [ - "@babel/register" - ] - }, - "babel": { - "presets": [ - "babel-preset-commitlint" - ] + "pkg": "pkg-check --skip-import" }, "engines": { "node": ">=4" @@ -62,13 +34,9 @@ }, "license": "MIT", "devDependencies": { - "@babel/core": "7.7.7", - "@babel/cli": "7.7.7", - "@babel/register": "7.7.7", "@commitlint/test": "8.2.0", "@commitlint/utils": "^8.3.4", - "ava": "2.4.0", - "babel-preset-commitlint": "^8.2.0", + "@types/cosmiconfig": "^5.0.3", "concurrently": "3.6.1", "cross-env": "6.0.3", "execa": "0.11.0", @@ -77,6 +45,7 @@ "dependencies": { "@commitlint/execute-rule": "^8.3.4", "@commitlint/resolve-extends": "^8.3.5", + "@commitlint/rules": "^8.3.4", "chalk": "2.4.2", "cosmiconfig": "^5.2.0", "lodash": "4.17.15", diff --git a/@commitlint/load/src/index.js b/@commitlint/load/src/index.js deleted file mode 100644 index e3f74d0d27..0000000000 --- a/@commitlint/load/src/index.js +++ /dev/null @@ -1,164 +0,0 @@ -import path from 'path'; -import executeRule from '@commitlint/execute-rule'; -import resolveExtends from '@commitlint/resolve-extends'; -import cosmiconfig from 'cosmiconfig'; -import {toPairs, merge, mergeWith, pick, startsWith} from 'lodash'; -import resolveFrom from 'resolve-from'; -import loadPlugin from './utils/loadPlugin'; - -const w = (a, b) => (Array.isArray(b) ? b : undefined); -const valid = input => - pick( - input, - 'extends', - 'rules', - 'plugins', - 'parserPreset', - 'formatter', - 'ignores', - 'defaultIgnores' - ); - -export default async (seed = {}, options = {cwd: process.cwd()}) => { - const loaded = await loadConfig(options.cwd, options.file); - const base = loaded.filepath ? path.dirname(loaded.filepath) : options.cwd; - - // Merge passed config with file based options - const config = valid(merge({}, loaded.config, seed)); - const opts = merge( - {extends: [], rules: {}, formatter: '@commitlint/format'}, - pick(config, 'extends', 'plugins', 'ignores', 'defaultIgnores') - ); - - // Resolve parserPreset key when overwritten by main config - if (typeof config.parserPreset === 'string') { - const resolvedParserPreset = resolveFrom(base, config.parserPreset); - - config.parserPreset = { - name: config.parserPreset, - path: resolvedParserPreset, - parserOpts: require(resolvedParserPreset) - }; - } - - // Resolve extends key - const extended = resolveExtends(opts, { - prefix: 'commitlint-config', - cwd: base, - parserPreset: config.parserPreset - }); - - const preset = valid(mergeWith(extended, config, w)); - - // Resolve parser-opts from preset - if (typeof preset.parserPreset === 'object') { - preset.parserPreset.parserOpts = await loadParserOpts( - preset.parserPreset.name, - preset.parserPreset - ); - } - - // Resolve config-relative formatter module - if (typeof config.formatter === 'string') { - preset.formatter = - resolveFrom.silent(base, config.formatter) || config.formatter; - } - - // resolve plugins - preset.plugins = {}; - if (config.plugins && config.plugins.length) { - config.plugins.forEach(pluginKey => { - loadPlugin(preset.plugins, pluginKey, process.env.DEBUG === 'true'); - }); - } - - // Execute rule config functions if needed - const executed = await Promise.all( - ['rules'] - .map(key => { - return [key, preset[key]]; - }) - .map(async item => { - const [key, value] = item; - const executedValue = await Promise.all( - toPairs(value || {}).map(entry => executeRule(entry)) - ); - return [ - key, - executedValue.reduce((registry, item) => { - const [key, value] = item; - registry[key] = value; - return registry; - }, {}) - ]; - }) - ); - - // Merge executed config keys into preset - return executed.reduce((registry, item) => { - const [key, value] = item; - registry[key] = value; - return registry; - }, preset); -}; - -async function loadConfig(cwd, configPath) { - const explorer = cosmiconfig('commitlint'); - - const explicitPath = configPath ? path.resolve(cwd, configPath) : undefined; - const explore = explicitPath ? explorer.load : explorer.search; - const searchPath = explicitPath ? explicitPath : cwd; - const local = await explore(searchPath); - - if (local) { - return local; - } - - return {}; -} - -async function loadParserOpts(parserName, pendingParser) { - // Await for the module, loaded with require - const parser = await pendingParser; - - // Await parser opts if applicable - if ( - typeof parser === 'object' && - typeof parser.parserOpts === 'object' && - typeof parser.parserOpts.then === 'function' - ) { - return (await parser.parserOpts).parserOpts; - } - - // Create parser opts from factory - if ( - typeof parser === 'object' && - typeof parser.parserOpts === 'function' && - startsWith(parserName, 'conventional-changelog-') - ) { - return await new Promise(resolve => { - const result = parser.parserOpts((_, opts) => { - resolve(opts.parserOpts); - }); - - // If result has data or a promise, the parser doesn't support factory-init - // due to https://github.com/nodejs/promises-debugging/issues/16 it just quits, so let's use this fallback - if (result) { - Promise.resolve(result).then(opts => { - resolve(opts.parserOpts); - }); - } - }); - } - - // Pull nested paserOpts, might happen if overwritten with a module in main config - if ( - typeof parser === 'object' && - typeof parser.parserOpts === 'object' && - typeof parser.parserOpts.parserOpts === 'object' - ) { - return parser.parserOpts.parserOpts; - } - - return parser.parserOpts; -} diff --git a/@commitlint/load/src/index.serial-test.js b/@commitlint/load/src/index.serial-test.js deleted file mode 100644 index 43f499ca66..0000000000 --- a/@commitlint/load/src/index.serial-test.js +++ /dev/null @@ -1,19 +0,0 @@ -import {fix} from '@commitlint/test'; -import test from 'ava'; - -import load from '.'; - -test.serial('default cwd option to process.cwd()', async t => { - const cwd = await fix.bootstrap('fixtures/basic'); - const before = process.cwd(); - process.chdir(cwd); - - try { - const actual = await load(); - t.true(actual.rules.basic); - } catch (err) { - throw err; - } finally { - process.chdir(before); - } -}); diff --git a/@commitlint/load/src/index.test.js b/@commitlint/load/src/index.test.js deleted file mode 100644 index 5407d33edf..0000000000 --- a/@commitlint/load/src/index.test.js +++ /dev/null @@ -1,384 +0,0 @@ -import path from 'path'; -import {fix, git, npm} from '@commitlint/test'; -import test from 'ava'; -import execa from 'execa'; -import resolveFrom from 'resolve-from'; - -import load from '.'; - -const proxyquire = require('proxyquire') - .noCallThru() - .noPreserveCache(); - -test('extends-empty should have no rules', async t => { - const cwd = await git.bootstrap('fixtures/extends-empty'); - const actual = await load({}, {cwd}); - t.deepEqual(actual.rules, {}); -}); - -test('uses seed as configured', async t => { - const cwd = await git.bootstrap('fixtures/extends-empty'); - const actual = await load({rules: {foo: 'bar'}}, {cwd}); - t.is(actual.rules.foo, 'bar'); -}); - -test('rules should be loaded from relative config file', async t => { - const file = 'config/commitlint.config.js'; - const cwd = await git.bootstrap('fixtures/specify-config-file'); - const actual = await load({}, {cwd, file}); - t.is(actual.rules.foo, 'bar'); -}); - -test('rules should be loaded from absolute config file', async t => { - const cwd = await git.bootstrap('fixtures/specify-config-file'); - const file = path.join(cwd, 'config/commitlint.config.js'); - const actual = await load({}, {cwd: process.cwd(), file}); - t.is(actual.rules.foo, 'bar'); -}); - -test('plugins should be loaded from seed', async t => { - const plugin = {'@global': true}; - const scopedPlugin = {'@global': true}; - const stubbedLoad = proxyquire('.', { - 'commitlint-plugin-example': plugin, - '@scope/commitlint-plugin-example': scopedPlugin - }).default; - - const cwd = await git.bootstrap('fixtures/extends-empty'); - const actual = await stubbedLoad( - {plugins: ['example', '@scope/example']}, - {cwd} - ); - t.deepEqual(actual.plugins, { - example: plugin, - '@scope/example': scopedPlugin - }); -}); - -test('plugins should be loaded from config', async t => { - const plugin = {'@global': true}; - const scopedPlugin = {'@global': true}; - const stubbedLoad = proxyquire('.', { - 'commitlint-plugin-example': plugin, - '@scope/commitlint-plugin-example': scopedPlugin - }).default; - - const cwd = await git.bootstrap('fixtures/extends-plugins'); - const actual = await stubbedLoad({}, {cwd}); - t.deepEqual(actual.plugins, { - example: plugin, - '@scope/example': scopedPlugin - }); -}); - -test('uses seed with parserPreset', async t => { - const cwd = await git.bootstrap('fixtures/parser-preset'); - const {parserPreset: actual} = await load( - { - parserPreset: './conventional-changelog-custom' - }, - {cwd} - ); - t.is(actual.name, './conventional-changelog-custom'); - t.deepEqual(actual.parserOpts, { - headerPattern: /^(\w*)(?:\((.*)\))?-(.*)$/ - }); -}); - -test('invalid extend should throw', async t => { - const cwd = await git.bootstrap('fixtures/extends-invalid'); - await t.throwsAsync(load({}, {cwd})); -}); - -test('empty file should have no rules', async t => { - const cwd = await git.bootstrap('fixtures/empty-object-file'); - const actual = await load({}, {cwd}); - t.deepEqual(actual.rules, {}); -}); - -test('empty file should extend nothing', async t => { - const cwd = await git.bootstrap('fixtures/empty-file'); - const actual = await load({}, {cwd}); - t.deepEqual(actual.extends, []); -}); - -test('respects cwd option', async t => { - const cwd = await git.bootstrap('fixtures/recursive-extends/first-extended'); - const actual = await load({}, {cwd}); - t.deepEqual(actual, { - formatter: '@commitlint/format', - extends: ['./second-extended'], - plugins: {}, - rules: { - one: 1, - two: 2 - } - }); -}); - -test('recursive extends', async t => { - const cwd = await git.bootstrap('fixtures/recursive-extends'); - const actual = await load({}, {cwd}); - t.deepEqual(actual, { - formatter: '@commitlint/format', - extends: ['./first-extended'], - plugins: {}, - rules: { - zero: 0, - one: 1, - two: 2 - } - }); -}); - -test('recursive extends with json file', async t => { - const cwd = await git.bootstrap('fixtures/recursive-extends-json'); - const actual = await load({}, {cwd}); - - t.deepEqual(actual, { - formatter: '@commitlint/format', - extends: ['./first-extended'], - plugins: {}, - rules: { - zero: 0, - one: 1, - two: 2 - } - }); -}); - -test('recursive extends with yaml file', async t => { - const cwd = await git.bootstrap('fixtures/recursive-extends-yaml'); - const actual = await load({}, {cwd}); - - t.deepEqual(actual, { - formatter: '@commitlint/format', - extends: ['./first-extended'], - plugins: {}, - rules: { - zero: 0, - one: 1, - two: 2 - } - }); -}); - -test('recursive extends with js file', async t => { - const cwd = await git.bootstrap('fixtures/recursive-extends-js'); - const actual = await load({}, {cwd}); - - t.deepEqual(actual, { - formatter: '@commitlint/format', - extends: ['./first-extended'], - plugins: {}, - rules: { - zero: 0, - one: 1, - two: 2 - } - }); -}); - -test('recursive extends with package.json file', async t => { - const cwd = await git.bootstrap('fixtures/recursive-extends-package'); - const actual = await load({}, {cwd}); - - t.deepEqual(actual, { - formatter: '@commitlint/format', - extends: ['./first-extended'], - plugins: {}, - rules: { - zero: 0, - one: 1, - two: 2 - } - }); -}); - -test('parser preset overwrites completely instead of merging', async t => { - const cwd = await git.bootstrap('fixtures/parser-preset-override'); - const actual = await load({}, {cwd}); - t.is(actual.parserPreset.name, './custom'); - t.deepEqual(actual.parserPreset.parserOpts, { - headerPattern: /.*/ - }); -}); - -test('recursive extends with parserPreset', async t => { - const cwd = await git.bootstrap('fixtures/recursive-parser-preset'); - const actual = await load({}, {cwd}); - t.is(actual.parserPreset.name, './conventional-changelog-custom'); - t.is(typeof actual.parserPreset.parserOpts, 'object'); - t.deepEqual( - actual.parserPreset.parserOpts.headerPattern, - /^(\w*)(?:\((.*)\))?-(.*)$/ - ); -}); - -test('ignores unknow keys', async t => { - const cwd = await git.bootstrap('fixtures/trash-file'); - const actual = await load({}, {cwd}); - - t.deepEqual(actual, { - formatter: '@commitlint/format', - extends: [], - plugins: {}, - rules: { - foo: 'bar', - baz: 'bar' - } - }); -}); - -test('ignores unknow keys recursively', async t => { - const cwd = await git.bootstrap('fixtures/trash-extend'); - const actual = await load({}, {cwd}); - - t.deepEqual(actual, { - formatter: '@commitlint/format', - extends: ['./one'], - plugins: {}, - rules: { - zero: 0, - one: 1 - } - }); -}); - -test('find up from given cwd', async t => { - const outer = await fix.bootstrap('fixtures/outer-scope'); - await git.init(path.join(outer, 'inner-scope')); - const cwd = path.join(outer, 'inner-scope', 'child-scope'); - - const actual = await load({}, {cwd}); - - t.deepEqual(actual, { - formatter: '@commitlint/format', - extends: [], - plugins: {}, - rules: { - child: true, - inner: false, - outer: false - } - }); -}); - -test('find up config from outside current git repo', async t => { - const outer = await fix.bootstrap('fixtures/outer-scope'); - const cwd = await git.init(path.join(outer, 'inner-scope')); - const actual = await load({}, {cwd}); - - t.deepEqual(actual, { - formatter: '@commitlint/format', - extends: [], - plugins: {}, - rules: { - child: false, - inner: false, - outer: true - } - }); -}); - -test('respects formatter option', async t => { - const cwd = await git.bootstrap('fixtures/formatter'); - const actual = await load({}, {cwd}); - - t.deepEqual(actual, { - formatter: 'commitlint-junit', - extends: [], - plugins: {}, - rules: {} - }); -}); - -test('resolves formatter relative from config directory', async t => { - const cwd = await git.bootstrap('fixtures/formatter-local-module'); - const actual = await load({}, {cwd}); - - t.deepEqual(actual, { - formatter: resolveFrom(cwd, './formatters/custom.js'), - extends: [], - plugins: {}, - rules: {} - }); -}); - -test('returns formatter name when unable to resolve from config directory', async t => { - const cwd = await git.bootstrap('fixtures/formatter-local-module'); - const actual = await load({formatter: './doesnt/exists.js'}, {cwd}); - - t.deepEqual(actual, { - formatter: './doesnt/exists.js', - extends: [], - plugins: {}, - rules: {} - }); -}); - -test('does not mutate config module reference', async t => { - const file = 'config/commitlint.config.js'; - const cwd = await git.bootstrap('fixtures/specify-config-file'); - - const configPath = path.join(cwd, file); - const before = JSON.stringify(require(configPath)); - await load({arbitraryField: true}, {cwd, file}); - const after = JSON.stringify(require(configPath)); - - t.is(before, after); -}); - -test('resolves parser preset from conventional commits', async t => { - const cwd = await npm.bootstrap('fixtures/parser-preset-conventionalcommits'); - const actual = await load({}, {cwd}); - - t.is(actual.parserPreset.name, 'conventional-changelog-conventionalcommits'); - t.is(typeof actual.parserPreset.parserOpts, 'object'); - t.deepEqual( - actual.parserPreset.parserOpts.headerPattern, - /^(\w*)(?:\((.*)\))?!?: (.*)$/ - ); -}); - -test('resolves parser preset from conventional angular', async t => { - const cwd = await npm.bootstrap('fixtures/parser-preset-angular'); - const actual = await load({}, {cwd}); - - t.is(actual.parserPreset.name, 'conventional-changelog-angular'); - t.is(typeof actual.parserPreset.parserOpts, 'object'); - t.deepEqual( - actual.parserPreset.parserOpts.headerPattern, - /^(\w*)(?:\((.*)\))?: (.*)$/ - ); -}); - -test('recursive resolves parser preset from conventional atom', async t => { - const cwd = await git.bootstrap( - 'fixtures/recursive-parser-preset-conventional-atom' - ); - // the package file is nested in 2 folders, `npm.bootstrap` cant do that - await execa('npm', ['install'], { - cwd: path.resolve(cwd, 'first-extended', 'second-extended') - }); - - const actual = await load({}, {cwd}); - - t.is(actual.parserPreset.name, 'conventional-changelog-atom'); - t.is(typeof actual.parserPreset.parserOpts, 'object'); - t.deepEqual(actual.parserPreset.parserOpts.headerPattern, /^(:.*?:) (.*)$/); -}); - -test('resolves parser preset from conventional commits without factory support', async t => { - const cwd = await npm.bootstrap( - 'fixtures/parser-preset-conventional-without-factory' - ); - const actual = await load({}, {cwd}); - - t.is(actual.parserPreset.name, 'conventional-changelog-conventionalcommits'); - t.is(typeof actual.parserPreset.parserOpts, 'object'); - t.deepEqual( - actual.parserPreset.parserOpts.headerPattern, - /^(\w*)(?:\((.*)\))?!?: (.*)$/ - ); -}); diff --git a/@commitlint/load/src/load.test.ts b/@commitlint/load/src/load.test.ts new file mode 100644 index 0000000000..4caeb1cf9e --- /dev/null +++ b/@commitlint/load/src/load.test.ts @@ -0,0 +1,394 @@ +const plugin = jest.fn(); +const scopedPlugin = jest.fn(); + +jest.mock('commitlint-plugin-example', () => plugin, {virtual: true}); +jest.mock('@scope/commitlint-plugin-example', () => scopedPlugin, { + virtual: true +}); + +import path from 'path'; +import execa from 'execa'; +import resolveFrom from 'resolve-from'; +import {fix, git, npm} from '@commitlint/test'; + +import load from './load'; +import {RuleSeverity} from './types'; + +const fixBootstrap = (name: string) => fix.bootstrap(name, __dirname); +const gitBootstrap = (name: string) => git.bootstrap(name, __dirname); +const npmBootstrap = (name: string) => npm.bootstrap(name, __dirname); + +test('extends-empty should have no rules', async () => { + const cwd = await gitBootstrap('fixtures/extends-empty'); + const actual = await load({}, {cwd}); + + expect(actual.rules).toMatchObject({}); +}); + +test('uses seed as configured', async () => { + const cwd = await gitBootstrap('fixtures/extends-empty'); + const rules = {'body-case': [1, 'never', 'camel-case'] as any}; + + const actual = await load({rules}, {cwd}); + + expect(actual.rules['body-case']).toStrictEqual([1, 'never', 'camel-case']); +}); + +test('rules should be loaded from relative config file', async () => { + const file = 'config/commitlint.config.js'; + const cwd = await gitBootstrap('fixtures/specify-config-file'); + const rules = {'body-case': [1, 'never', 'camel-case'] as any}; + + const actual = await load({rules}, {cwd, file}); + + expect(actual.rules['body-case']).toStrictEqual([1, 'never', 'camel-case']); +}); + +test('rules should be loaded from absolute config file', async () => { + const cwd = await gitBootstrap('fixtures/specify-config-file'); + const file = path.resolve(cwd, 'config/commitlint.config.js'); + const rules = {'body-case': [1, 'never', 'camel-case'] as any}; + + const actual = await load({rules}, {cwd: process.cwd(), file}); + + expect(actual.rules['body-case']).toStrictEqual([1, 'never', 'camel-case']); +}); + +test('plugins should be loaded from seed', async () => { + const cwd = await gitBootstrap('fixtures/extends-empty'); + const actual = await load({plugins: ['example', '@scope/example']}, {cwd}); + + expect(actual.plugins).toMatchObject({ + example: plugin, + '@scope/example': scopedPlugin + }); +}); + +test('plugins should be loaded from config', async () => { + const cwd = await gitBootstrap('fixtures/extends-plugins'); + const actual = await load({}, {cwd}); + + expect(actual.plugins).toMatchObject({ + example: plugin, + '@scope/example': scopedPlugin + }); +}); + +test('uses seed with parserPreset', async () => { + const cwd = await gitBootstrap('fixtures/parser-preset'); + const {parserPreset: actual} = await load( + {parserPreset: './conventional-changelog-custom'}, + {cwd} + ); + + expect(actual.name).toBe('./conventional-changelog-custom'); + expect(actual.parserOpts).toMatchObject({ + headerPattern: /^(\w*)(?:\((.*)\))?-(.*)$/ + }); +}); + +test('invalid extend should throw', async () => { + const cwd = await gitBootstrap('fixtures/extends-invalid'); + + await expect(load({}, {cwd})).rejects.toThrow(); +}); + +test('empty file should have no rules', async () => { + const cwd = await gitBootstrap('fixtures/empty-object-file'); + const actual = await load({}, {cwd}); + + expect(actual.rules).toMatchObject({}); +}); + +test('empty file should extend nothing', async () => { + const cwd = await gitBootstrap('fixtures/empty-file'); + const actual = await load({}, {cwd}); + + expect(actual.extends).toHaveLength(0); +}); + +test('respects cwd option', async () => { + const cwd = await gitBootstrap('fixtures/recursive-extends/first-extended'); + const actual = await load({}, {cwd}); + + expect(actual).toMatchObject({ + formatter: '@commitlint/format', + extends: ['./second-extended'], + plugins: {}, + rules: { + one: 1, + two: 2 + } + }); +}); + +test('recursive extends', async () => { + const cwd = await gitBootstrap('fixtures/recursive-extends'); + const actual = await load({}, {cwd}); + + expect(actual).toMatchObject({ + formatter: '@commitlint/format', + extends: ['./first-extended'], + plugins: {}, + rules: { + zero: 0, + one: 1, + two: 2 + } + }); +}); + +test('recursive extends with json file', async () => { + const cwd = await gitBootstrap('fixtures/recursive-extends-json'); + const actual = await load({}, {cwd}); + + expect(actual).toMatchObject({ + formatter: '@commitlint/format', + extends: ['./first-extended'], + plugins: {}, + rules: { + zero: 0, + one: 1, + two: 2 + } + }); +}); + +test('recursive extends with yaml file', async () => { + const cwd = await gitBootstrap('fixtures/recursive-extends-yaml'); + const actual = await load({}, {cwd}); + + expect(actual).toMatchObject({ + formatter: '@commitlint/format', + extends: ['./first-extended'], + plugins: {}, + rules: { + zero: 0, + one: 1, + two: 2 + } + }); +}); + +test('recursive extends with js file', async () => { + const cwd = await gitBootstrap('fixtures/recursive-extends-js'); + const actual = await load({}, {cwd}); + + expect(actual).toMatchObject({ + formatter: '@commitlint/format', + extends: ['./first-extended'], + plugins: {}, + rules: { + zero: 0, + one: 1, + two: 2 + } + }); +}); + +test('recursive extends with package.json file', async () => { + const cwd = await gitBootstrap('fixtures/recursive-extends-package'); + const actual = await load({}, {cwd}); + + expect(actual).toMatchObject({ + formatter: '@commitlint/format', + extends: ['./first-extended'], + plugins: {}, + rules: { + zero: 0, + one: 1, + two: 2 + } + }); +}); + +test('parser preset overwrites completely instead of merging', async () => { + const cwd = await gitBootstrap('fixtures/parser-preset-override'); + const actual = await load({}, {cwd}); + + expect(actual.parserPreset.name).toBe('./custom'); + expect(actual.parserPreset.parserOpts).toMatchObject({ + headerPattern: /.*/ + }); +}); + +test('recursive extends with parserPreset', async () => { + const cwd = await gitBootstrap('fixtures/recursive-parser-preset'); + const actual = await load({}, {cwd}); + + expect(actual.parserPreset.name).toBe('./conventional-changelog-custom'); + expect(actual.parserPreset.parserOpts).toMatchObject({ + headerPattern: /^(\w*)(?:\((.*)\))?-(.*)$/ + }); +}); + +test('ignores unknow keys', async () => { + const cwd = await gitBootstrap('fixtures/trash-file'); + const actual = await load({}, {cwd}); + + expect(actual).toMatchObject({ + formatter: '@commitlint/format', + extends: [], + plugins: {}, + rules: { + foo: 'bar', + baz: 'bar' + } + }); +}); + +test('ignores unknow keys recursively', async () => { + const cwd = await gitBootstrap('fixtures/trash-extend'); + const actual = await load({}, {cwd}); + + expect(actual).toMatchObject({ + formatter: '@commitlint/format', + extends: ['./one'], + plugins: {}, + rules: { + zero: 0, + one: 1 + } + }); +}); + +test('find up from given cwd', async () => { + const outer = await fixBootstrap('fixtures/outer-scope'); + await git.init(path.join(outer, 'inner-scope')); + const cwd = path.join(outer, 'inner-scope', 'child-scope'); + const actual = await load({}, {cwd}); + + expect(actual).toMatchObject({ + formatter: '@commitlint/format', + extends: [], + plugins: {}, + rules: { + child: true, + inner: false, + outer: false + } + }); +}); + +test('find up config from outside current git repo', async () => { + const outer = await fixBootstrap('fixtures/outer-scope'); + const cwd = await git.init(path.join(outer, 'inner-scope')); + const actual = await load({}, {cwd}); + + expect(actual).toMatchObject({ + formatter: '@commitlint/format', + extends: [], + plugins: {}, + rules: { + child: false, + inner: false, + outer: true + } + }); +}); + +test('respects formatter option', async () => { + const cwd = await gitBootstrap('fixtures/formatter'); + const actual = await load({}, {cwd}); + + expect(actual).toMatchObject({ + formatter: 'commitlint-junit', + extends: [], + plugins: {}, + rules: {} + }); +}); + +test('resolves formatter relative from config directory', async () => { + const cwd = await gitBootstrap('fixtures/formatter-local-module'); + const actual = await load({}, {cwd}); + + expect(actual).toMatchObject({ + formatter: resolveFrom(cwd, './formatters/custom.js'), + extends: [], + plugins: {}, + rules: {} + }); +}); + +test('returns formatter name when unable to resolve from config directory', async () => { + const cwd = await gitBootstrap('fixtures/formatter-local-module'); + const actual = await load({formatter: './doesnt/exists.js'}, {cwd}); + + expect(actual).toMatchObject({ + formatter: './doesnt/exists.js', + extends: [], + plugins: {}, + rules: {} + }); +}); + +test('does not mutate config module reference', async () => { + const file = 'config/commitlint.config.js'; + const cwd = await gitBootstrap('fixtures/specify-config-file'); + const rules = {'body-case': [1, 'never', 'camel-case'] as any}; + + const configPath = path.join(cwd, file); + const before = JSON.stringify(require(configPath)); + await load({rules}, {cwd, file}); + const after = JSON.stringify(require(configPath)); + + expect(after).toBe(before); +}); + +test('resolves parser preset from conventional commits', async () => { + const cwd = await npmBootstrap('fixtures/parser-preset-conventionalcommits'); + const actual = await load({}, {cwd}); + + expect(actual.parserPreset.name).toBe( + 'conventional-changelog-conventionalcommits' + ); + expect(typeof actual.parserPreset.parserOpts).toBe('object'); + expect((actual.parserPreset.parserOpts as any).headerPattern).toEqual( + /^(\w*)(?:\((.*)\))?!?: (.*)$/ + ); +}); + +test('resolves parser preset from conventional angular', async () => { + const cwd = await npmBootstrap('fixtures/parser-preset-angular'); + const actual = await load({}, {cwd}); + + expect(actual.parserPreset.name).toBe('conventional-changelog-angular'); + expect(typeof actual.parserPreset.parserOpts).toBe('object'); + expect((actual.parserPreset.parserOpts as any).headerPattern).toEqual( + /^(\w*)(?:\((.*)\))?: (.*)$/ + ); +}); + +test('recursive resolves parser preset from conventional atom', async () => { + const cwd = await gitBootstrap( + 'fixtures/recursive-parser-preset-conventional-atom' + ); + // the package file is nested in 2 folders, `npm.bootstrap` cant do that + await execa('npm', ['install'], { + cwd: path.resolve(cwd, 'first-extended', 'second-extended') + }); + + const actual = await load({}, {cwd}); + + expect(actual.parserPreset.name).toBe('conventional-changelog-atom'); + expect(typeof actual.parserPreset.parserOpts).toBe('object'); + expect((actual.parserPreset.parserOpts as any).headerPattern).toEqual( + /^(:.*?:) (.*)$/ + ); +}); + +test('resolves parser preset from conventional commits without factory support', async () => { + const cwd = await npmBootstrap( + 'fixtures/parser-preset-conventional-without-factory' + ); + const actual = await load({}, {cwd}); + + expect(actual.parserPreset.name).toBe( + 'conventional-changelog-conventionalcommits' + ); + expect(typeof actual.parserPreset.parserOpts).toBe('object'); + expect((actual.parserPreset.parserOpts as any).headerPattern).toEqual( + /^(\w*)(?:\((.*)\))?!?: (.*)$/ + ); +}); diff --git a/@commitlint/load/src/load.ts b/@commitlint/load/src/load.ts new file mode 100644 index 0000000000..0d961e0d05 --- /dev/null +++ b/@commitlint/load/src/load.ts @@ -0,0 +1,107 @@ +import Path from 'path'; + +import {toPairs, merge, mergeWith, pick} from 'lodash'; +import resolveFrom from 'resolve-from'; + +import executeRule from '@commitlint/execute-rule'; +import resolveExtends from '@commitlint/resolve-extends'; + +import loadPlugin from './utils/loadPlugin'; +import { + UserConfig, + LoadOptions, + QualifiedConfig, + UserPreset, + QualifiedRules, + ParserPreset +} from './types'; +import {loadConfig} from './utils/load-config'; +import {loadParserOpts} from './utils/load-parser-opts'; +import {pickConfig} from './utils/pick-config'; + +const w = (_: unknown, b: ArrayLike | null | undefined | false) => + Array.isArray(b) ? b : undefined; + +export default async function load( + seed: UserConfig = {}, + options: LoadOptions = {} +): Promise { + 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; + + // Merge passed config with file based options + const config = pickConfig(merge({}, loaded ? loaded.config : null, seed)); + + const opts = merge( + {extends: [], rules: {}, formatter: '@commitlint/format'}, + pick(config, 'extends', 'plugins', 'ignores', 'defaultIgnores') + ); + + // Resolve parserPreset key + if (typeof config.parserPreset === 'string') { + const resolvedParserPreset = resolveFrom(base, config.parserPreset); + + config.parserPreset = { + name: config.parserPreset, + path: resolvedParserPreset, + parserOpts: require(resolvedParserPreset) + }; + } + + // Resolve extends key + const extended = resolveExtends(opts, { + prefix: 'commitlint-config', + cwd: base, + parserPreset: config.parserPreset + }); + + const preset = (pickConfig( + mergeWith(extended, config, w) + ) as unknown) as UserPreset; + preset.plugins = {}; + + // TODO: check if this is still necessary with the new factory based conventional changelog parsers + // config.extends = Array.isArray(config.extends) ? config.extends : []; + + // Resolve parser-opts from preset + if (typeof preset.parserPreset === 'object') { + preset.parserPreset.parserOpts = await loadParserOpts( + preset.parserPreset.name, + // TODO: fix the types for factory based conventional changelog parsers + preset.parserPreset as any + ); + } + + // Resolve config-relative formatter module + if (typeof config.formatter === 'string') { + preset.formatter = + resolveFrom.silent(base, config.formatter) || config.formatter; + } + + // resolve plugins + if (Array.isArray(config.plugins)) { + config.plugins.forEach((pluginKey: string) => { + loadPlugin(preset.plugins, pluginKey, process.env.DEBUG === 'true'); + }); + } + + const rules = preset.rules ? preset.rules : {}; + const qualifiedRules = (await Promise.all( + toPairs(rules || {}).map(entry => executeRule(entry)) + )).reduce((registry, item) => { + const [key, value] = item as any; + (registry as any)[key] = value; + return registry; + }, {}); + + return { + extends: preset.extends!, + formatter: preset.formatter!, + parserPreset: preset.parserPreset! as ParserPreset, + ignores: preset.ignores!, + defaultIgnores: preset.defaultIgnores!, + plugins: preset.plugins!, + rules: qualifiedRules + }; +} diff --git a/@commitlint/load/src/types.ts b/@commitlint/load/src/types.ts new file mode 100644 index 0000000000..77960194a7 --- /dev/null +++ b/@commitlint/load/src/types.ts @@ -0,0 +1,123 @@ +import {TargetCaseType} from '@commitlint/ensure'; +import {RuleCondition} from '@commitlint/rules'; + +export {RuleCondition} from '@commitlint/rules'; + +export type PluginRecords = Record; + +export interface LoadOptions { + cwd?: string; + file?: string; +} + +export enum RuleSeverity { + Warning = 1, + Error = 2 +} + +export type RuleConfigTuple = ReadonlyArray< + T extends void + ? [RuleSeverity, RuleCondition] + : [RuleSeverity, RuleCondition, T] +>; + +export enum RuleConfigQuality { + User, + Qualified +} + +export type QualifiedRuleConfig = + | (() => RuleConfigTuple) + | (() => RuleConfigTuple>) + | RuleConfigTuple; + +export type RuleConfig< + V = RuleConfigQuality.Qualified, + T = void +> = V extends false ? RuleConfigTuple : QualifiedRuleConfig; + +export type CaseRuleConfig = RuleConfig< + V, + TargetCaseType +>; +export type LengthRuleConfig = RuleConfig< + V, + number +>; +export type EnumRuleConfig = RuleConfig< + V, + string[] +>; + +export type RulesConfig = { + 'body-case': CaseRuleConfig; + 'body-empty': RuleConfig; + 'body-leading-blank': RuleConfig; + 'body-max-length': LengthRuleConfig; + 'body-max-line-length': LengthRuleConfig; + 'body-min-length': LengthRuleConfig; + 'footer-empty': RuleConfig; + 'footer-leading-blank': RuleConfig; + 'footer-max-length': LengthRuleConfig; + 'footer-max-line-length': LengthRuleConfig; + 'footer-min-length': LengthRuleConfig; + 'header-case': CaseRuleConfig; + 'header-full-stop': RuleConfig; + 'header-max-length': LengthRuleConfig; + 'header-min-length': LengthRuleConfig; + 'references-empty': RuleConfig; + 'scope-case': CaseRuleConfig; + 'scope-empty': RuleConfig; + 'scope-enum': EnumRuleConfig; + 'scope-max-length': LengthRuleConfig; + 'scope-min-length': LengthRuleConfig; + 'signed-off-by': RuleConfig; + 'subject-case': CaseRuleConfig; + 'subject-empty': RuleConfig; + 'subject-full-stop': RuleConfig; + 'subject-max-length': LengthRuleConfig; + 'subject-min-length': LengthRuleConfig; + 'type-case': CaseRuleConfig; + 'type-empty': RuleConfig; + 'type-enum': EnumRuleConfig; + 'type-max-length': LengthRuleConfig; + 'type-min-length': LengthRuleConfig; +}; + +export interface UserConfig { + extends?: string[]; + formatter?: unknown; + rules?: Partial; + parserPreset?: string | ParserPreset; + ignores?: ((commit: string) => boolean)[]; + defaultIgnores?: boolean; + plugins?: string[]; +} + +export interface UserPreset { + extends?: string[]; + formatter?: unknown; + rules?: Partial; + parserPreset?: string | ParserPreset; + ignores?: ((commit: string) => boolean)[]; + defaultIgnores?: boolean; + plugins: PluginRecords; +} + +export type QualifiedRules = Partial>; + +export interface QualifiedConfig { + extends: string[]; + formatter: unknown; + rules: Partial; + parserPreset: ParserPreset; + ignores: ((commit: string) => boolean)[]; + defaultIgnores: boolean; + plugins: PluginRecords; +} + +export interface ParserPreset { + name: string; + path: string; + parserOpts?: unknown; +} diff --git a/@commitlint/load/src/utils/load-config.ts b/@commitlint/load/src/utils/load-config.ts new file mode 100644 index 0000000000..5f191d73ae --- /dev/null +++ b/@commitlint/load/src/utils/load-config.ts @@ -0,0 +1,21 @@ +import path from 'path'; +import {CosmiconfigResult} from 'cosmiconfig'; +import cosmiconfig from 'cosmiconfig'; + +export async function loadConfig( + cwd: string, + configPath?: string +): Promise { + const explorer = cosmiconfig('commitlint'); + + const explicitPath = configPath ? path.resolve(cwd, configPath) : undefined; + const explore = explicitPath ? explorer.load : explorer.search; + const searchPath = explicitPath ? explicitPath : cwd; + const local = await explore(searchPath); + + if (local) { + return local; + } + + return null; +} diff --git a/@commitlint/load/src/utils/load-parser-opts.ts b/@commitlint/load/src/utils/load-parser-opts.ts new file mode 100644 index 0000000000..788c77112f --- /dev/null +++ b/@commitlint/load/src/utils/load-parser-opts.ts @@ -0,0 +1,50 @@ +import {startsWith} from 'lodash'; + +export async function loadParserOpts( + parserName: string, + pendingParser: Promise +) { + // Await for the module, loaded with require + const parser = await pendingParser; + + // Await parser opts if applicable + if ( + typeof parser === 'object' && + typeof parser.parserOpts === 'object' && + typeof parser.parserOpts.then === 'function' + ) { + return (await parser.parserOpts).parserOpts; + } + + // Create parser opts from factory + if ( + typeof parser === 'object' && + typeof parser.parserOpts === 'function' && + startsWith(parserName, 'conventional-changelog-') + ) { + return await new Promise(resolve => { + const result = parser.parserOpts((_: never, opts: {parserOpts: any}) => { + resolve(opts.parserOpts); + }); + + // If result has data or a promise, the parser doesn't support factory-init + // due to https://github.com/nodejs/promises-debugging/issues/16 it just quits, so let's use this fallback + if (result) { + Promise.resolve(result).then(opts => { + resolve(opts.parserOpts); + }); + } + }); + } + + // Pull nested paserOpts, might happen if overwritten with a module in main config + if ( + typeof parser === 'object' && + typeof parser.parserOpts === 'object' && + typeof parser.parserOpts.parserOpts === 'object' + ) { + return parser.parserOpts.parserOpts; + } + + return parser.parserOpts; +} diff --git a/@commitlint/load/src/utils/loadPlugin.test.js b/@commitlint/load/src/utils/loadPlugin.test.js deleted file mode 100644 index e4e188732a..0000000000 --- a/@commitlint/load/src/utils/loadPlugin.test.js +++ /dev/null @@ -1,80 +0,0 @@ -import test from 'ava'; -const proxyquire = require('proxyquire') - .noCallThru() - .noPreserveCache(); - -test.beforeEach(t => { - const plugins = {}; - const plugin = {}; - const scopedPlugin = {}; - const stubbedLoadPlugin = proxyquire('./loadPlugin', { - 'commitlint-plugin-example': plugin, - '@scope/commitlint-plugin-example': scopedPlugin - }).default; - t.context.data = { - plugins, - plugin, - scopedPlugin, - stubbedLoadPlugin - }; -}); - -test('should load a plugin when referenced by short name', t => { - const {stubbedLoadPlugin, plugins, plugin} = t.context.data; - stubbedLoadPlugin(plugins, 'example'); - t.is(plugins['example'], plugin); -}); - -test('should load a plugin when referenced by long name', t => { - const {stubbedLoadPlugin, plugins, plugin} = t.context.data; - stubbedLoadPlugin(plugins, 'commitlint-plugin-example'); - t.is(plugins['example'], plugin); -}); - -test('should throw an error when a plugin has whitespace', t => { - const {stubbedLoadPlugin, plugins} = t.context.data; - t.throws(() => { - stubbedLoadPlugin(plugins, 'whitespace '); - }, /Whitespace found in plugin name 'whitespace '/u); - t.throws(() => { - stubbedLoadPlugin(plugins, 'whitespace\t'); - }, /Whitespace found in plugin name/u); - t.throws(() => { - stubbedLoadPlugin(plugins, 'whitespace\n'); - }, /Whitespace found in plugin name/u); - t.throws(() => { - stubbedLoadPlugin(plugins, 'whitespace\r'); - }, /Whitespace found in plugin name/u); -}); - -test("should throw an error when a plugin doesn't exist", t => { - const {stubbedLoadPlugin, plugins} = t.context.data; - t.throws(() => { - stubbedLoadPlugin(plugins, 'nonexistentplugin'); - }, /Failed to load plugin/u); -}); - -test('should load a scoped plugin when referenced by short name', t => { - const {stubbedLoadPlugin, plugins, scopedPlugin} = t.context.data; - stubbedLoadPlugin(plugins, '@scope/example'); - t.is(plugins['@scope/example'], scopedPlugin); -}); - -test('should load a scoped plugin when referenced by long name', t => { - const {stubbedLoadPlugin, plugins, scopedPlugin} = t.context.data; - stubbedLoadPlugin(plugins, '@scope/commitlint-plugin-example'); - t.is(plugins['@scope/example'], scopedPlugin); -}); - -/* when referencing a scope plugin and omitting @scope/ */ -test("should load a scoped plugin when referenced by short name, but should not get the plugin if '@scope/' is omitted", t => { - const {stubbedLoadPlugin, plugins} = t.context.data; - stubbedLoadPlugin(plugins, '@scope/example'); - t.is(plugins['example'], undefined); -}); - -test("should load a scoped plugin when referenced by long name, but should not get the plugin if '@scope/' is omitted", t => { - const {stubbedLoadPlugin, plugins} = t.context.data; - stubbedLoadPlugin(plugins, '@scope/commitlint-plugin-example'); - t.is(plugins['example'], undefined); -}); diff --git a/@commitlint/load/src/utils/loadPlugin.test.ts b/@commitlint/load/src/utils/loadPlugin.test.ts new file mode 100644 index 0000000000..49f074b3c7 --- /dev/null +++ b/@commitlint/load/src/utils/loadPlugin.test.ts @@ -0,0 +1,65 @@ +import loadPlugin from './loadPlugin'; + +jest.mock('commitlint-plugin-example', () => ({example: true}), { + virtual: true +}); + +jest.mock('@scope/commitlint-plugin-example', () => ({scope: true}), { + virtual: true +}); + +test('should load a plugin when referenced by short name', () => { + const plugins = loadPlugin({}, 'example'); + expect(plugins['example']).toBe(require('commitlint-plugin-example')); +}); + +test('should load a plugin when referenced by long name', () => { + const plugins = loadPlugin({}, 'commitlint-plugin-example'); + expect(plugins['example']).toBe(require('commitlint-plugin-example')); +}); + +test('should throw an error when a plugin has whitespace', () => { + expect(() => loadPlugin({}, 'whitespace ')).toThrow( + "Whitespace found in plugin name 'whitespace '" + ); + expect(() => loadPlugin({}, 'whitespace\t')).toThrow( + 'Whitespace found in plugin name' + ); + expect(() => loadPlugin({}, 'whitespace\n')).toThrow( + 'Whitespace found in plugin name' + ); + expect(() => loadPlugin({}, 'whitespace\r')).toThrow( + 'Whitespace found in plugin name' + ); +}); + +test("should throw an error when a plugin doesn't exist", () => { + expect(() => loadPlugin({}, 'nonexistentplugin')).toThrow( + 'Failed to load plugin' + ); +}); + +test('should load a scoped plugin when referenced by short name', () => { + const plugins = loadPlugin({}, '@scope/example'); + expect(plugins['@scope/example']).toBe( + require('@scope/commitlint-plugin-example') + ); +}); + +test('should load a scoped plugin when referenced by long name', () => { + const plugins = loadPlugin({}, '@scope/commitlint-plugin-example'); + expect(plugins['@scope/example']).toBe( + require('@scope/commitlint-plugin-example') + ); +}); + +/* when referencing a scope plugin and omitting @scope/ */ +test("should load a scoped plugin when referenced by short name, but should not get the plugin if '@scope/' is omitted", () => { + const plugins = loadPlugin({}, '@scope/example'); + expect(plugins['example']).toBe(undefined); +}); + +test("should load a scoped plugin when referenced by long name, but should not get the plugin if '@scope/' is omitted", () => { + const plugins = loadPlugin({}, '@scope/commitlint-plugin-example'); + expect(plugins['example']).toBe(undefined); +}); diff --git a/@commitlint/load/src/utils/loadPlugin.js b/@commitlint/load/src/utils/loadPlugin.ts similarity index 72% rename from @commitlint/load/src/utils/loadPlugin.js rename to @commitlint/load/src/utils/loadPlugin.ts index 416e0a8784..724f2f7ebc 100644 --- a/@commitlint/load/src/utils/loadPlugin.js +++ b/@commitlint/load/src/utils/loadPlugin.ts @@ -1,22 +1,22 @@ import path from 'path'; import chalk from 'chalk'; import {normalizePackageName, getShorthandName} from './pluginNaming'; +import {WhitespacePluginError, MissingPluginError} from './pluginErrors'; +import {PluginRecords} from '../types'; -export default function loadPlugin(plugins, pluginName, debug = false) { +export default function loadPlugin( + plugins: PluginRecords, + pluginName: string, + debug: boolean = false +): PluginRecords { const longName = normalizePackageName(pluginName); const shortName = getShorthandName(longName); let plugin = null; if (pluginName.match(/\s+/u)) { - const whitespaceError = new Error( - `Whitespace found in plugin name '${pluginName}'` - ); - - whitespaceError.messageTemplate = 'whitespace-found'; - whitespaceError.messageData = { + throw new WhitespacePluginError(pluginName, { pluginName: longName - }; - throw whitespaceError; + }); } const pluginKey = longName === pluginName ? shortName : pluginName; @@ -28,18 +28,14 @@ export default function loadPlugin(plugins, pluginName, debug = false) { try { // Check whether the plugin exists require.resolve(longName); - } catch (missingPluginErr) { + } catch (error) { // If the plugin can't be resolved, display the missing plugin error (usually a config or install error) console.error(chalk.red(`Failed to load plugin ${longName}.`)); - missingPluginErr.message = `Failed to load plugin ${pluginName}: ${ - missingPluginErr.message - }`; - missingPluginErr.messageTemplate = 'plugin-missing'; - missingPluginErr.messageData = { + + throw new MissingPluginError(pluginName, error.message, { pluginName: longName, commitlintPath: path.resolve(__dirname, '../..') - }; - throw missingPluginErr; + }); } // Otherwise, the plugin exists and is throwing on module load for some reason, so print the stack trace. @@ -71,4 +67,6 @@ export default function loadPlugin(plugins, pluginName, debug = false) { plugins[pluginKey] = plugin; } + + return plugins; } diff --git a/@commitlint/load/src/utils/pick-config.ts b/@commitlint/load/src/utils/pick-config.ts new file mode 100644 index 0000000000..04074cbb87 --- /dev/null +++ b/@commitlint/load/src/utils/pick-config.ts @@ -0,0 +1,14 @@ +import {UserConfig} from '../types'; +import {pick} from 'lodash'; + +export const pickConfig = (input: unknown): UserConfig => + pick( + input, + 'extends', + 'rules', + 'plugins', + 'parserPreset', + 'formatter', + 'ignores', + 'defaultIgnores' + ); diff --git a/@commitlint/load/src/utils/pluginErrors.ts b/@commitlint/load/src/utils/pluginErrors.ts new file mode 100644 index 0000000000..4c7b1f0e29 --- /dev/null +++ b/@commitlint/load/src/utils/pluginErrors.ts @@ -0,0 +1,29 @@ +export class WhitespacePluginError extends Error { + __proto__ = Error; + + public messageTemplate: string = 'whitespace-found'; + public messageData: any = {}; + + constructor(pluginName?: string, data: any = {}) { + super(`Whitespace found in plugin name '${pluginName}'`); + + this.messageData = data; + + Object.setPrototypeOf(this, WhitespacePluginError.prototype); + } +} + +export class MissingPluginError extends Error { + __proto__ = Error; + + public messageTemplate: string = 'plugin-missing'; + public messageData: any; + + constructor(pluginName?: string, errorMessage: string = '', data: any = {}) { + super(`Failed to load plugin ${pluginName}: ${errorMessage}`); + + this.messageData = data; + + Object.setPrototypeOf(this, MissingPluginError.prototype); + } +} diff --git a/@commitlint/load/src/utils/pluginNaming.js b/@commitlint/load/src/utils/pluginNaming.ts similarity index 91% rename from @commitlint/load/src/utils/pluginNaming.js rename to @commitlint/load/src/utils/pluginNaming.ts index 84dc2938f1..ecf42784b4 100644 --- a/@commitlint/load/src/utils/pluginNaming.js +++ b/@commitlint/load/src/utils/pluginNaming.ts @@ -1,10 +1,12 @@ +import path from 'path'; + // largely adapted from eslint's plugin system const NAMESPACE_REGEX = /^@.*\//iu; // In eslint this is a parameter - we don't need to support the extra options const prefix = 'commitlint-plugin'; // Replace Windows with posix style paths -function convertPathToPosix(filepath) { +function convertPathToPosix(filepath: string) { const normalizedFilepath = path.normalize(filepath); const posixFilepath = normalizedFilepath.replace(/\\/gu, '/'); @@ -17,7 +19,7 @@ function convertPathToPosix(filepath) { * @returns {string} Normalized name of the package * @private */ -export function normalizePackageName(name) { +export function normalizePackageName(name: string) { let normalizedName = name; /** @@ -67,7 +69,7 @@ export function normalizePackageName(name) { * @param {string} fullname The term which may have the prefix. * @returns {string} The term without prefix. */ -export function getShorthandName(fullname) { +export function getShorthandName(fullname: string) { if (fullname[0] === '@') { let matchResult = new RegExp(`^(@[^/]+)/${prefix}$`, 'u').exec(fullname); @@ -91,7 +93,7 @@ export function getShorthandName(fullname) { * @param {string} term The term which may have the namespace. * @returns {string} The namepace of the term if it has one. */ -export function getNamespaceFromTerm(term) { +export function getNamespaceFromTerm(term: string) { const match = term.match(NAMESPACE_REGEX); return match ? match[0] : ''; diff --git a/@commitlint/load/tsconfig.json b/@commitlint/load/tsconfig.json new file mode 100644 index 0000000000..955cf565b8 --- /dev/null +++ b/@commitlint/load/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.shared.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": [ + "./src" + ], + "exclude": [ + "./src/**/*.test.ts", + "./lib/**/*" + ], + "references": [ + { "path": "../execute-rule" }, + { "path": "../resolve-extends" }, + { "path": "../rules" } + ] +} diff --git a/@commitlint/rules/package.json b/@commitlint/rules/package.json index b607e279a1..a0de10644d 100644 --- a/@commitlint/rules/package.json +++ b/@commitlint/rules/package.json @@ -44,6 +44,7 @@ "dependencies": { "@commitlint/ensure": "^8.3.4", "@commitlint/message": "^8.3.4", + "@commitlint/parse": "^8.3.4", "@commitlint/to-lines": "^8.3.4" } } diff --git a/@commitlint/rules/tsconfig.json b/@commitlint/rules/tsconfig.json index f4a57643f0..7824a8d568 100644 --- a/@commitlint/rules/tsconfig.json +++ b/@commitlint/rules/tsconfig.json @@ -11,5 +11,11 @@ "exclude": [ "./src/**/*.test.ts", "./lib/**/*" - ] + ], + "references": [ + { "path": "../ensure" }, + { "path": "../message" }, + { "path": "../parse" }, + { "path": "../to-lines" } + ] } diff --git a/jest.config.js b/jest.config.js index fbb71fb849..dc4efc120d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,11 +4,7 @@ module.exports = { testRegex: undefined, testMatch: [ '**/*.test.ts?(x)', - '**/@commitlint/lint/src/*.test.js?(x)', - '**/@commitlint/read/src/*.test.js?(x)', - '**/@commitlint/travis-cli/src/*.test.js?(x)', - '**/@commitlint/cli/src/*.test.js?(x)', - '**/@commitlint/prompt-cli/*.test.js?(x)', - '**/@commitlint/prompt/src/**/*.test.js?(x)' + '**/@commitlint/{lint,read,travis-cli,cli,load,prompt}/src/*.test.js?(x)', + '**/@commitlint/prompt-cli/*.test.js?(x)' ] }; diff --git a/tsconfig.json b/tsconfig.json index bece0d1aff..5a4d1574a4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ { "path": "@commitlint/execute-rule" }, { "path": "@commitlint/format" }, { "path": "@commitlint/is-ignored" }, + { "path": "@commitlint/load" }, { "path": "@commitlint/message" }, { "path": "@commitlint/parse" }, { "path": "@commitlint/resolve-extends" }, diff --git a/tsconfig.shared.json b/tsconfig.shared.json index 50fcc95422..9b015b776e 100644 --- a/tsconfig.shared.json +++ b/tsconfig.shared.json @@ -6,8 +6,9 @@ "declaration": true, "declarationMap": true, "sourceMap": true, + "module": "commonjs", "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 7f8a743ed8..2e321b3d26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1806,6 +1806,13 @@ resolved "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/cosmiconfig@^5.0.3": + version "5.0.3" + resolved "https://registry.npmjs.org/@types/cosmiconfig/-/cosmiconfig-5.0.3.tgz#880644bb155d4038d3b752159684b777b0a159dd" + integrity sha512-HgTGG7X5y9pLl3pixeo2XtDEFD8rq2EuH+S4mK6teCnAwWMucQl6v1D43hI4Uw1VJh6nu59lxLkqXHRl4uwThA== + dependencies: + "@types/node" "*" + "@types/events@*": version "3.0.0" resolved "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"