From 04fa0b207f70247ef09eb31b2c4ca1d2e09cbc59 Mon Sep 17 00:00:00 2001 From: Cedric van Putten Date: Sat, 27 Jul 2019 13:46:00 +0000 Subject: [PATCH 01/13] refactor: port load to typescript [wip] --- @commitlint/load/package.json | 39 +- ...ex.serial-test.js => index.serial-test.ts} | 3 +- @commitlint/load/src/index.test.js | 329 ----------------- @commitlint/load/src/index.test.ts | 340 ++++++++++++++++++ @commitlint/load/src/{index.js => index.ts} | 32 +- ...{loadPlugin.test.js => loadPlugin.test.ts} | 0 .../utils/{loadPlugin.js => loadPlugin.ts} | 29 +- @commitlint/load/src/utils/pluginErrors.ts | 29 ++ .../{pluginNaming.js => pluginNaming.ts} | 10 +- @commitlint/load/tsconfig.json | 15 + tsconfig.json | 3 +- yarn.lock | 7 + 12 files changed, 438 insertions(+), 398 deletions(-) rename @commitlint/load/src/{index.serial-test.js => index.serial-test.ts} (89%) delete mode 100644 @commitlint/load/src/index.test.js create mode 100644 @commitlint/load/src/index.test.ts rename @commitlint/load/src/{index.js => index.ts} (77%) rename @commitlint/load/src/utils/{loadPlugin.test.js => loadPlugin.test.ts} (100%) rename @commitlint/load/src/utils/{loadPlugin.js => loadPlugin.ts} (72%) create mode 100644 @commitlint/load/src/utils/pluginErrors.ts rename @commitlint/load/src/utils/{pluginNaming.js => pluginNaming.ts} (91%) create mode 100644 @commitlint/load/tsconfig.json diff --git a/@commitlint/load/package.json b/@commitlint/load/package.json index e8e488352b..6ae4ec244e 100644 --- a/@commitlint/load/package.json +++ b/@commitlint/load/package.json @@ -3,35 +3,13 @@ "version": "8.1.0", "description": "Load shared commitlint configuration", "main": "lib/index.js", + "types": "lib/index.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/*.serial-test.js\" --verbose", - "watch": "babel src --out-dir lib --watch --source-maps" - }, - "ava": { - "files": [ - "src/**/*.test.js", - "!lib/**/*" - ], - "source": [ - "src/**/*.js", - "!lib/**/*" - ], - "babel": "inherit", - "require": [ - "babel-register" - ] - }, - "babel": { - "presets": [ - "babel-preset-commitlint" - ] + "pkg": "pkg-check --skip-import" }, "engines": { "node": ">=4" @@ -58,19 +36,16 @@ "devDependencies": { "@commitlint/test": "8.0.0", "@commitlint/utils": "^8.1.0", - "ava": "0.22.0", - "babel-cli": "6.26.0", - "babel-preset-commitlint": "^8.0.0", - "babel-register": "6.26.0", - "concurrently": "3.6.1", - "cross-env": "5.1.1", + "@types/cosmiconfig": "5.0.3", + "@types/lodash": "4.14.136", "execa": "0.11.0", - "globby": "10.0.1" + "globby": "10.0.1", + "proxyquire": "2.1.1", + "typescript": "3.5.3" }, "dependencies": { "@commitlint/execute-rule": "^8.1.0", "@commitlint/resolve-extends": "^8.1.0", - "babel-runtime": "^6.23.0", "chalk": "2.4.2", "cosmiconfig": "^5.2.0", "lodash": "4.17.14", diff --git a/@commitlint/load/src/index.serial-test.js b/@commitlint/load/src/index.serial-test.ts similarity index 89% rename from @commitlint/load/src/index.serial-test.js rename to @commitlint/load/src/index.serial-test.ts index 43f499ca66..21ecc55f5d 100644 --- a/@commitlint/load/src/index.serial-test.js +++ b/@commitlint/load/src/index.serial-test.ts @@ -1,6 +1,7 @@ -import {fix} from '@commitlint/test'; import test from 'ava'; +const {fix} = require('@commitlint/test'); + import load from '.'; test.serial('default cwd option to process.cwd()', async t => { diff --git a/@commitlint/load/src/index.test.js b/@commitlint/load/src/index.test.js deleted file mode 100644 index 4dbf31444b..0000000000 --- a/@commitlint/load/src/index.test.js +++ /dev/null @@ -1,329 +0,0 @@ -import path from 'path'; -import {fix, git} from '@commitlint/test'; -import test from 'ava'; -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 - }); - - 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 - }); - - 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.throws(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); -}); diff --git a/@commitlint/load/src/index.test.ts b/@commitlint/load/src/index.test.ts new file mode 100644 index 0000000000..e9f5f7fbcd --- /dev/null +++ b/@commitlint/load/src/index.test.ts @@ -0,0 +1,340 @@ +import path from 'path'; +import resolveFrom from 'resolve-from'; + +const {fix, git} = require('@commitlint/test'); + +import load from '.'; + +const proxyquire = require('proxyquire') + .noCallThru() + .noPreserveCache(); + +const fixture = (name: string) => path.resolve(__dirname, '../fixtures', name); + +test('extends-empty should have no rules', async () => { + const cwd = await git.bootstrap(fixture('extends-empty')); + const actual = await load({}, {cwd}); + + expect(actual.rules).toMatchObject({}); +}); + +test('uses seed as configured', async () => { + const cwd = await git.bootstrap(fixture('extends-empty')); + const actual = await load({rules: {foo: 'bar'}}, {cwd}); + + expect(actual.rules.foo).toBe('bar'); +}); + +test('rules should be loaded from relative config file', async () => { + const file = 'config/commitlint.config.js'; + const cwd = await git.bootstrap(fixture('specify-config-file')); + const actual = await load({}, {cwd, file}); + + expect(actual.rules.foo).toBe('bar'); +}); + +test('rules should be loaded from absolute config file', async () => { + const cwd = await git.bootstrap(fixture('specify-config-file')); + const file = path.resolve(cwd, 'config/commitlint.config.js'); + const actual = await load({}, {cwd: process.cwd(), file}); + + expect(actual.rules.foo).toBe('bar'); +}); + +// test('plugins should be loaded from seed', async () => { +// const plugin = {'@global': true}; +// const scopedPlugin = {'@global': true}; +// const {default: stubbedLoad} = proxyquire('../.', { +// 'commitlint-plugin-example': plugin, +// '@scope/commitlint-plugin-example': scopedPlugin +// }); + +// const cwd = await git.bootstrap(fixture('extends-empty')); +// const actual = await stubbedLoad( +// {plugins: ['example', '@scope/example']}, +// {cwd} +// ); + +// expect(actual.plugins).toBe({ +// example: plugin, +// '@scope/example': scopedPlugin +// }); +// }); + +// test('plugins should be loaded from config', async () => { +// const plugin = {'@global': true}; +// const scopedPlugin = {'@global': true}; +// const stubbedLoad = proxyquire('.', { +// 'commitlint-plugin-example': plugin, +// '@scope/commitlint-plugin-example': scopedPlugin +// }); + +// const cwd = await git.bootstrap(fixture('extends-plugins')); +// const actual = await stubbedLoad({}, {cwd}); +// t.deepEqual(actual.plugins, { +// example: plugin, +// '@scope/example': scopedPlugin +// }); +// }); + +test('uses seed with parserPreset', async () => { + const cwd = await git.bootstrap(fixture('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 git.bootstrap(fixture('extends-invalid')); +// await t.throws(load({}, {cwd})); +// }); + +test('empty file should have no rules', async () => { + const cwd = await git.bootstrap(fixture('empty-object-file')); + const actual = await load({}, {cwd}); + + expect(actual.rules).toMatchObject({}); +}); + +test('empty file should extend nothing', async () => { + const cwd = await git.bootstrap(fixture('empty-file')); + const actual = await load({}, {cwd}); + + expect(actual.extends).toHaveLength(0); +}); + +test('respects cwd option', async () => { + const cwd = await git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 fix.bootstrap(fixture('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 fix.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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)); + + expect(after).toBe(before); +}); diff --git a/@commitlint/load/src/index.js b/@commitlint/load/src/index.ts similarity index 77% rename from @commitlint/load/src/index.js rename to @commitlint/load/src/index.ts index 1fb4c789f5..fc066a5cab 100644 --- a/@commitlint/load/src/index.js +++ b/@commitlint/load/src/index.ts @@ -1,13 +1,13 @@ import path from 'path'; import executeRule from '@commitlint/execute-rule'; import resolveExtends from '@commitlint/resolve-extends'; -import cosmiconfig from 'cosmiconfig'; +import cosmiconfig, {CosmiconfigResult} from 'cosmiconfig'; import {toPairs, merge, mergeWith, pick} from 'lodash'; import resolveFrom from 'resolve-from'; import loadPlugin from './utils/loadPlugin'; -const w = (a, b) => (Array.isArray(b) ? b : undefined); -const valid = input => +const w = (a: any, b: any) => (Array.isArray(b) ? b : undefined); +const valid = (input: any) => pick( input, 'extends', @@ -19,12 +19,13 @@ const valid = input => 'defaultIgnores' ); -export default async (seed = {}, options = {cwd: process.cwd()}) => { +export default async (seed: any = {}, options: any = {cwd: process.cwd()}) => { const loaded = await loadConfig(options.cwd, options.file); - const base = loaded.filepath ? path.dirname(loaded.filepath) : options.cwd; + const base = + loaded && loaded.filepath ? path.dirname(loaded.filepath) : options.cwd; // Merge passed config with file based options - const config = valid(merge({}, loaded.config, seed)); + const config = valid(merge({}, loaded ? loaded.config : null, seed)); const opts = merge( {extends: [], rules: {}, formatter: '@commitlint/format'}, pick(config, 'extends', 'plugins', 'ignores', 'defaultIgnores') @@ -68,7 +69,7 @@ export default async (seed = {}, options = {cwd: process.cwd()}) => { // resolve plugins preset.plugins = {}; if (config.plugins && config.plugins.length) { - config.plugins.forEach(pluginKey => { + config.plugins.forEach((pluginKey: string) => { loadPlugin(preset.plugins, pluginKey, process.env.DEBUG === 'true'); }); } @@ -77,18 +78,18 @@ export default async (seed = {}, options = {cwd: process.cwd()}) => { const executed = await Promise.all( ['rules'] .map(key => { - return [key, preset[key]]; + return [key, (preset as any)[key]]; }) .map(async item => { const [key, value] = item; const executedValue = await Promise.all( - toPairs(value || {}).map(entry => executeRule(entry)) + toPairs(value || {}).map(entry => executeRule(entry)) ); return [ key, executedValue.reduce((registry, item) => { - const [key, value] = item; - registry[key] = value; + const [key, value] = item as any; + (registry as any)[key] = value; return registry; }, {}) ]; @@ -98,12 +99,15 @@ export default async (seed = {}, options = {cwd: process.cwd()}) => { // Merge executed config keys into preset return executed.reduce((registry, item) => { const [key, value] = item; - registry[key] = value; + (registry as any)[key] = value; return registry; }, preset); }; -async function loadConfig(cwd, configPath) { +async function loadConfig( + cwd: string, + configPath?: string +): Promise { const explorer = cosmiconfig('commitlint'); const explicitPath = configPath ? path.resolve(cwd, configPath) : undefined; @@ -115,5 +119,5 @@ async function loadConfig(cwd, configPath) { return local; } - return {}; + return null; } diff --git a/@commitlint/load/src/utils/loadPlugin.test.js b/@commitlint/load/src/utils/loadPlugin.test.ts similarity index 100% rename from @commitlint/load/src/utils/loadPlugin.test.js rename to @commitlint/load/src/utils/loadPlugin.test.ts 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..f693eab9c3 100644 --- a/@commitlint/load/src/utils/loadPlugin.js +++ b/@commitlint/load/src/utils/loadPlugin.ts @@ -1,22 +1,21 @@ import path from 'path'; import chalk from 'chalk'; import {normalizePackageName, getShorthandName} from './pluginNaming'; +import {WhitespacePluginError, MissingPluginError} from './pluginErrors'; -export default function loadPlugin(plugins, pluginName, debug = false) { +export default function loadPlugin( + plugins: any, + pluginName: string, + debug: boolean = false +) { 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 +27,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. 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..f4a57643f0 --- /dev/null +++ b/@commitlint/load/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.shared.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": [ + "./src" + ], + "exclude": [ + "./src/**/*.test.ts", + "./lib/**/*" + ] +} diff --git a/tsconfig.json b/tsconfig.json index 48225a91cc..fa08b94c7e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,8 +7,9 @@ { "path": "@commitlint/execute-rule" }, { "path": "@commitlint/format" }, { "path": "@commitlint/is-ignored" }, + { "path": "@commitlint/load" }, { "path": "@commitlint/resolve-extends" }, { "path": "@commitlint/to-lines" }, { "path": "@commitlint/top-level" }, ] -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 922c12fd4e..cf9a737100 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1234,6 +1234,13 @@ dependencies: "@babel/types" "^7.3.0" +"@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" From a8ab88959c0a027dfa827b62aa949f11b899b853 Mon Sep 17 00:00:00 2001 From: Cedric van Putten Date: Sat, 27 Jul 2019 13:46:00 +0000 Subject: [PATCH 02/13] refactor: port load to typescript [wip] --- @commitlint/load/package.json | 39 +- ...ex.serial-test.js => index.serial-test.ts} | 3 +- @commitlint/load/src/index.test.js | 329 ----------------- @commitlint/load/src/index.test.ts | 340 ++++++++++++++++++ @commitlint/load/src/{index.js => index.ts} | 32 +- ...{loadPlugin.test.js => loadPlugin.test.ts} | 0 .../utils/{loadPlugin.js => loadPlugin.ts} | 29 +- @commitlint/load/src/utils/pluginErrors.ts | 29 ++ .../{pluginNaming.js => pluginNaming.ts} | 10 +- @commitlint/load/tsconfig.json | 15 + tsconfig.json | 1 + yarn.lock | 7 + 12 files changed, 437 insertions(+), 397 deletions(-) rename @commitlint/load/src/{index.serial-test.js => index.serial-test.ts} (89%) delete mode 100644 @commitlint/load/src/index.test.js create mode 100644 @commitlint/load/src/index.test.ts rename @commitlint/load/src/{index.js => index.ts} (77%) rename @commitlint/load/src/utils/{loadPlugin.test.js => loadPlugin.test.ts} (100%) rename @commitlint/load/src/utils/{loadPlugin.js => loadPlugin.ts} (72%) create mode 100644 @commitlint/load/src/utils/pluginErrors.ts rename @commitlint/load/src/utils/{pluginNaming.js => pluginNaming.ts} (91%) create mode 100644 @commitlint/load/tsconfig.json diff --git a/@commitlint/load/package.json b/@commitlint/load/package.json index e8e488352b..6ae4ec244e 100644 --- a/@commitlint/load/package.json +++ b/@commitlint/load/package.json @@ -3,35 +3,13 @@ "version": "8.1.0", "description": "Load shared commitlint configuration", "main": "lib/index.js", + "types": "lib/index.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/*.serial-test.js\" --verbose", - "watch": "babel src --out-dir lib --watch --source-maps" - }, - "ava": { - "files": [ - "src/**/*.test.js", - "!lib/**/*" - ], - "source": [ - "src/**/*.js", - "!lib/**/*" - ], - "babel": "inherit", - "require": [ - "babel-register" - ] - }, - "babel": { - "presets": [ - "babel-preset-commitlint" - ] + "pkg": "pkg-check --skip-import" }, "engines": { "node": ">=4" @@ -58,19 +36,16 @@ "devDependencies": { "@commitlint/test": "8.0.0", "@commitlint/utils": "^8.1.0", - "ava": "0.22.0", - "babel-cli": "6.26.0", - "babel-preset-commitlint": "^8.0.0", - "babel-register": "6.26.0", - "concurrently": "3.6.1", - "cross-env": "5.1.1", + "@types/cosmiconfig": "5.0.3", + "@types/lodash": "4.14.136", "execa": "0.11.0", - "globby": "10.0.1" + "globby": "10.0.1", + "proxyquire": "2.1.1", + "typescript": "3.5.3" }, "dependencies": { "@commitlint/execute-rule": "^8.1.0", "@commitlint/resolve-extends": "^8.1.0", - "babel-runtime": "^6.23.0", "chalk": "2.4.2", "cosmiconfig": "^5.2.0", "lodash": "4.17.14", diff --git a/@commitlint/load/src/index.serial-test.js b/@commitlint/load/src/index.serial-test.ts similarity index 89% rename from @commitlint/load/src/index.serial-test.js rename to @commitlint/load/src/index.serial-test.ts index 43f499ca66..21ecc55f5d 100644 --- a/@commitlint/load/src/index.serial-test.js +++ b/@commitlint/load/src/index.serial-test.ts @@ -1,6 +1,7 @@ -import {fix} from '@commitlint/test'; import test from 'ava'; +const {fix} = require('@commitlint/test'); + import load from '.'; test.serial('default cwd option to process.cwd()', async t => { diff --git a/@commitlint/load/src/index.test.js b/@commitlint/load/src/index.test.js deleted file mode 100644 index 4dbf31444b..0000000000 --- a/@commitlint/load/src/index.test.js +++ /dev/null @@ -1,329 +0,0 @@ -import path from 'path'; -import {fix, git} from '@commitlint/test'; -import test from 'ava'; -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 - }); - - 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 - }); - - 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.throws(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); -}); diff --git a/@commitlint/load/src/index.test.ts b/@commitlint/load/src/index.test.ts new file mode 100644 index 0000000000..e9f5f7fbcd --- /dev/null +++ b/@commitlint/load/src/index.test.ts @@ -0,0 +1,340 @@ +import path from 'path'; +import resolveFrom from 'resolve-from'; + +const {fix, git} = require('@commitlint/test'); + +import load from '.'; + +const proxyquire = require('proxyquire') + .noCallThru() + .noPreserveCache(); + +const fixture = (name: string) => path.resolve(__dirname, '../fixtures', name); + +test('extends-empty should have no rules', async () => { + const cwd = await git.bootstrap(fixture('extends-empty')); + const actual = await load({}, {cwd}); + + expect(actual.rules).toMatchObject({}); +}); + +test('uses seed as configured', async () => { + const cwd = await git.bootstrap(fixture('extends-empty')); + const actual = await load({rules: {foo: 'bar'}}, {cwd}); + + expect(actual.rules.foo).toBe('bar'); +}); + +test('rules should be loaded from relative config file', async () => { + const file = 'config/commitlint.config.js'; + const cwd = await git.bootstrap(fixture('specify-config-file')); + const actual = await load({}, {cwd, file}); + + expect(actual.rules.foo).toBe('bar'); +}); + +test('rules should be loaded from absolute config file', async () => { + const cwd = await git.bootstrap(fixture('specify-config-file')); + const file = path.resolve(cwd, 'config/commitlint.config.js'); + const actual = await load({}, {cwd: process.cwd(), file}); + + expect(actual.rules.foo).toBe('bar'); +}); + +// test('plugins should be loaded from seed', async () => { +// const plugin = {'@global': true}; +// const scopedPlugin = {'@global': true}; +// const {default: stubbedLoad} = proxyquire('../.', { +// 'commitlint-plugin-example': plugin, +// '@scope/commitlint-plugin-example': scopedPlugin +// }); + +// const cwd = await git.bootstrap(fixture('extends-empty')); +// const actual = await stubbedLoad( +// {plugins: ['example', '@scope/example']}, +// {cwd} +// ); + +// expect(actual.plugins).toBe({ +// example: plugin, +// '@scope/example': scopedPlugin +// }); +// }); + +// test('plugins should be loaded from config', async () => { +// const plugin = {'@global': true}; +// const scopedPlugin = {'@global': true}; +// const stubbedLoad = proxyquire('.', { +// 'commitlint-plugin-example': plugin, +// '@scope/commitlint-plugin-example': scopedPlugin +// }); + +// const cwd = await git.bootstrap(fixture('extends-plugins')); +// const actual = await stubbedLoad({}, {cwd}); +// t.deepEqual(actual.plugins, { +// example: plugin, +// '@scope/example': scopedPlugin +// }); +// }); + +test('uses seed with parserPreset', async () => { + const cwd = await git.bootstrap(fixture('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 git.bootstrap(fixture('extends-invalid')); +// await t.throws(load({}, {cwd})); +// }); + +test('empty file should have no rules', async () => { + const cwd = await git.bootstrap(fixture('empty-object-file')); + const actual = await load({}, {cwd}); + + expect(actual.rules).toMatchObject({}); +}); + +test('empty file should extend nothing', async () => { + const cwd = await git.bootstrap(fixture('empty-file')); + const actual = await load({}, {cwd}); + + expect(actual.extends).toHaveLength(0); +}); + +test('respects cwd option', async () => { + const cwd = await git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 fix.bootstrap(fixture('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 fix.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('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)); + + expect(after).toBe(before); +}); diff --git a/@commitlint/load/src/index.js b/@commitlint/load/src/index.ts similarity index 77% rename from @commitlint/load/src/index.js rename to @commitlint/load/src/index.ts index 1fb4c789f5..fc066a5cab 100644 --- a/@commitlint/load/src/index.js +++ b/@commitlint/load/src/index.ts @@ -1,13 +1,13 @@ import path from 'path'; import executeRule from '@commitlint/execute-rule'; import resolveExtends from '@commitlint/resolve-extends'; -import cosmiconfig from 'cosmiconfig'; +import cosmiconfig, {CosmiconfigResult} from 'cosmiconfig'; import {toPairs, merge, mergeWith, pick} from 'lodash'; import resolveFrom from 'resolve-from'; import loadPlugin from './utils/loadPlugin'; -const w = (a, b) => (Array.isArray(b) ? b : undefined); -const valid = input => +const w = (a: any, b: any) => (Array.isArray(b) ? b : undefined); +const valid = (input: any) => pick( input, 'extends', @@ -19,12 +19,13 @@ const valid = input => 'defaultIgnores' ); -export default async (seed = {}, options = {cwd: process.cwd()}) => { +export default async (seed: any = {}, options: any = {cwd: process.cwd()}) => { const loaded = await loadConfig(options.cwd, options.file); - const base = loaded.filepath ? path.dirname(loaded.filepath) : options.cwd; + const base = + loaded && loaded.filepath ? path.dirname(loaded.filepath) : options.cwd; // Merge passed config with file based options - const config = valid(merge({}, loaded.config, seed)); + const config = valid(merge({}, loaded ? loaded.config : null, seed)); const opts = merge( {extends: [], rules: {}, formatter: '@commitlint/format'}, pick(config, 'extends', 'plugins', 'ignores', 'defaultIgnores') @@ -68,7 +69,7 @@ export default async (seed = {}, options = {cwd: process.cwd()}) => { // resolve plugins preset.plugins = {}; if (config.plugins && config.plugins.length) { - config.plugins.forEach(pluginKey => { + config.plugins.forEach((pluginKey: string) => { loadPlugin(preset.plugins, pluginKey, process.env.DEBUG === 'true'); }); } @@ -77,18 +78,18 @@ export default async (seed = {}, options = {cwd: process.cwd()}) => { const executed = await Promise.all( ['rules'] .map(key => { - return [key, preset[key]]; + return [key, (preset as any)[key]]; }) .map(async item => { const [key, value] = item; const executedValue = await Promise.all( - toPairs(value || {}).map(entry => executeRule(entry)) + toPairs(value || {}).map(entry => executeRule(entry)) ); return [ key, executedValue.reduce((registry, item) => { - const [key, value] = item; - registry[key] = value; + const [key, value] = item as any; + (registry as any)[key] = value; return registry; }, {}) ]; @@ -98,12 +99,15 @@ export default async (seed = {}, options = {cwd: process.cwd()}) => { // Merge executed config keys into preset return executed.reduce((registry, item) => { const [key, value] = item; - registry[key] = value; + (registry as any)[key] = value; return registry; }, preset); }; -async function loadConfig(cwd, configPath) { +async function loadConfig( + cwd: string, + configPath?: string +): Promise { const explorer = cosmiconfig('commitlint'); const explicitPath = configPath ? path.resolve(cwd, configPath) : undefined; @@ -115,5 +119,5 @@ async function loadConfig(cwd, configPath) { return local; } - return {}; + return null; } diff --git a/@commitlint/load/src/utils/loadPlugin.test.js b/@commitlint/load/src/utils/loadPlugin.test.ts similarity index 100% rename from @commitlint/load/src/utils/loadPlugin.test.js rename to @commitlint/load/src/utils/loadPlugin.test.ts 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..f693eab9c3 100644 --- a/@commitlint/load/src/utils/loadPlugin.js +++ b/@commitlint/load/src/utils/loadPlugin.ts @@ -1,22 +1,21 @@ import path from 'path'; import chalk from 'chalk'; import {normalizePackageName, getShorthandName} from './pluginNaming'; +import {WhitespacePluginError, MissingPluginError} from './pluginErrors'; -export default function loadPlugin(plugins, pluginName, debug = false) { +export default function loadPlugin( + plugins: any, + pluginName: string, + debug: boolean = false +) { 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 +27,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. 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..f4a57643f0 --- /dev/null +++ b/@commitlint/load/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.shared.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": [ + "./src" + ], + "exclude": [ + "./src/**/*.test.ts", + "./lib/**/*" + ] +} diff --git a/tsconfig.json b/tsconfig.json index 02b03b0b40..3156509908 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,6 @@ { "path": "@commitlint/resolve-extends" }, { "path": "@commitlint/to-lines" }, { "path": "@commitlint/top-level" }, + { "path": "@commitlint/load" }, ] } diff --git a/yarn.lock b/yarn.lock index f829f9b116..70c11bedb2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1234,6 +1234,13 @@ dependencies: "@babel/types" "^7.3.0" +"@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" From ec0363720212b5887b4243cafb5e7c45a565c84c Mon Sep 17 00:00:00 2001 From: Cedric van Putten Date: Thu, 12 Sep 2019 21:39:56 +0000 Subject: [PATCH 03/13] refactor(load): finish port to typescript --- @commitlint/load/package.json | 4 +- @commitlint/load/src/index.serial-test.ts | 20 --- @commitlint/load/src/index.serial.test.ts | 21 ++++ @commitlint/load/src/index.test.ts | 79 +++++------- @commitlint/load/src/utils/loadPlugin.test.ts | 115 ++++++++---------- yarn.lock | 2 +- 6 files changed, 110 insertions(+), 131 deletions(-) delete mode 100644 @commitlint/load/src/index.serial-test.ts create mode 100644 @commitlint/load/src/index.serial.test.ts diff --git a/@commitlint/load/package.json b/@commitlint/load/package.json index 6ae4ec244e..da8839ee47 100644 --- a/@commitlint/load/package.json +++ b/@commitlint/load/package.json @@ -38,9 +38,7 @@ "@commitlint/utils": "^8.1.0", "@types/cosmiconfig": "5.0.3", "@types/lodash": "4.14.136", - "execa": "0.11.0", - "globby": "10.0.1", - "proxyquire": "2.1.1", + "@types/resolve-from": "^5.0.1", "typescript": "3.5.3" }, "dependencies": { diff --git a/@commitlint/load/src/index.serial-test.ts b/@commitlint/load/src/index.serial-test.ts deleted file mode 100644 index 21ecc55f5d..0000000000 --- a/@commitlint/load/src/index.serial-test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import test from 'ava'; - -const {fix} = require('@commitlint/test'); - -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.serial.test.ts b/@commitlint/load/src/index.serial.test.ts new file mode 100644 index 0000000000..8e6dca17b5 --- /dev/null +++ b/@commitlint/load/src/index.serial.test.ts @@ -0,0 +1,21 @@ +import * as path from 'path'; +import load from '.'; + +const {fix} = require('@commitlint/test'); + +const fixture = (name: string) => path.resolve(__dirname, '../fixtures', name); + +test('default cwd option to process.cwd()', async () => { + const cwd = await fix.bootstrap(fixture('basic')); + const before = process.cwd(); + process.chdir(cwd); + + try { + const actual = await load(); + expect(actual.rules.basic).toBeTruthy(); + } catch (err) { + throw err; + } finally { + process.chdir(before); + } +}); diff --git a/@commitlint/load/src/index.test.ts b/@commitlint/load/src/index.test.ts index e9f5f7fbcd..6678138f4a 100644 --- a/@commitlint/load/src/index.test.ts +++ b/@commitlint/load/src/index.test.ts @@ -1,3 +1,11 @@ +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 resolveFrom from 'resolve-from'; @@ -5,10 +13,6 @@ const {fix, git} = require('@commitlint/test'); import load from '.'; -const proxyquire = require('proxyquire') - .noCallThru() - .noPreserveCache(); - const fixture = (name: string) => path.resolve(__dirname, '../fixtures', name); test('extends-empty should have no rules', async () => { @@ -41,48 +45,30 @@ test('rules should be loaded from absolute config file', async () => { expect(actual.rules.foo).toBe('bar'); }); -// test('plugins should be loaded from seed', async () => { -// const plugin = {'@global': true}; -// const scopedPlugin = {'@global': true}; -// const {default: stubbedLoad} = proxyquire('../.', { -// 'commitlint-plugin-example': plugin, -// '@scope/commitlint-plugin-example': scopedPlugin -// }); - -// const cwd = await git.bootstrap(fixture('extends-empty')); -// const actual = await stubbedLoad( -// {plugins: ['example', '@scope/example']}, -// {cwd} -// ); - -// expect(actual.plugins).toBe({ -// example: plugin, -// '@scope/example': scopedPlugin -// }); -// }); - -// test('plugins should be loaded from config', async () => { -// const plugin = {'@global': true}; -// const scopedPlugin = {'@global': true}; -// const stubbedLoad = proxyquire('.', { -// 'commitlint-plugin-example': plugin, -// '@scope/commitlint-plugin-example': scopedPlugin -// }); - -// const cwd = await git.bootstrap(fixture('extends-plugins')); -// const actual = await stubbedLoad({}, {cwd}); -// t.deepEqual(actual.plugins, { -// example: plugin, -// '@scope/example': scopedPlugin -// }); -// }); +test('plugins should be loaded from seed', async () => { + const cwd = await git.bootstrap(fixture('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 git.bootstrap(fixture('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 git.bootstrap(fixture('parser-preset')); const {parserPreset: actual} = await load( - { - parserPreset: './conventional-changelog-custom' - }, + {parserPreset: './conventional-changelog-custom'}, {cwd} ); @@ -92,10 +78,11 @@ test('uses seed with parserPreset', async () => { }); }); -// test('invalid extend should throw', async () => { -// const cwd = await git.bootstrap(fixture('extends-invalid')); -// await t.throws(load({}, {cwd})); -// }); +test('invalid extend should throw', async () => { + const cwd = await git.bootstrap(fixture('extends-invalid')); + + await expect(load({}, {cwd})).rejects.toThrow(); +}); test('empty file should have no rules', async () => { const cwd = await git.bootstrap(fixture('empty-object-file')); diff --git a/@commitlint/load/src/utils/loadPlugin.test.ts b/@commitlint/load/src/utils/loadPlugin.test.ts index 9af7382f72..b1856ab6bc 100644 --- a/@commitlint/load/src/utils/loadPlugin.test.ts +++ b/@commitlint/load/src/utils/loadPlugin.test.ts @@ -1,80 +1,73 @@ -import test from 'ava'; -const proxyquire = require('proxyquire') - .noCallThru() - .noPreserveCache(); +const commitlintPluginExample = jest.fn(); +const scopedCommitlintPluginExample = jest.fn(); -test.beforeEach(t => { - const plugins = {}; - const plugin = {}; - const scopedPlugin = {}; - const stubbedLoadPlugin = proxyquire('./loadPlugin', { - 'commitlint-plugin-example': plugin, - '@scope/commitlint-plugin-example': scopedPlugin - }); - t.context.data = { - plugins, - plugin, - scopedPlugin, - stubbedLoadPlugin - }; +jest.mock('commitlint-plugin-example', () => commitlintPluginExample, { + virtual: true }); +jest.mock( + '@scope/commitlint-plugin-example', + () => scopedCommitlintPluginExample, + {virtual: true} +); -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); +import loadPlugin from './loadPlugin'; + +test('should load a plugin when referenced by short name', () => { + const plugins: any = {}; + loadPlugin(plugins, 'example'); + expect(plugins['example']).toBe(commitlintPluginExample); }); -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 load a plugin when referenced by long name', () => { + const plugins: any = {}; + loadPlugin(plugins, 'commitlint-plugin-example'); + expect(plugins['example']).toBe(commitlintPluginExample); }); -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 has whitespace', () => { + const plugins: any = {}; + expect(() => loadPlugin(plugins, 'whitespace ')).toThrow( + /Whitespace found in plugin name 'whitespace '/u + ); + expect(() => loadPlugin(plugins, 'whitespace\t')).toThrow( + /Whitespace found in plugin name/u + ); + expect(() => loadPlugin(plugins, 'whitespace\n')).toThrow( + /Whitespace found in plugin name/u + ); + expect(() => loadPlugin(plugins, 'whitespace\r')).toThrow( + /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 throw an error when a plugin doesn't exist", () => { + const plugins: any = {}; + expect(() => loadPlugin(plugins, 'nonexistentplugin')).toThrow( + /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 short name', () => { + const plugins: any = {}; + loadPlugin(plugins, '@scope/example'); + expect(plugins['@scope/example']).toBe(scopedCommitlintPluginExample); }); -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); +test('should load a scoped plugin when referenced by long name', () => { + const plugins: any = {}; + loadPlugin(plugins, '@scope/commitlint-plugin-example'); + expect(plugins['@scope/example']).toBe(scopedCommitlintPluginExample); }); /* 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 short name, but should not get the plugin if '@scope/' is omitted", () => { + const plugins: any = {}; + loadPlugin(plugins, '@scope/example'); + expect(plugins['example']).toBeUndefined(); }); -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); +test("should load a scoped plugin when referenced by long name, but should not get the plugin if '@scope/' is omitted", () => { + const plugins: any = {}; + loadPlugin(plugins, '@scope/commitlint-plugin-example'); + expect(plugins['example']).toBeUndefined(); }); diff --git a/yarn.lock b/yarn.lock index 70c11bedb2..247496055d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1317,7 +1317,7 @@ resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== -"@types/resolve-from@5.0.1": +"@types/resolve-from@5.0.1", "@types/resolve-from@^5.0.1": version "5.0.1" resolved "https://registry.npmjs.org/@types/resolve-from/-/resolve-from-5.0.1.tgz#2714eaa840c0472dcfa96ec3fb9d170dbf0b677d" integrity sha512-1G7n5Jtr5inoS1Ez2Y9Efedk9/wH6uGQslbfhGTOw9J42PCAwuyaDgQHW7fIq02+shwB02kM/w31W8gMxI8ORg== From 5b08411ca63df4ea89f510899718dccdf487a6a6 Mon Sep 17 00:00:00 2001 From: Cedric van Putten Date: Thu, 12 Sep 2019 21:40:16 +0000 Subject: [PATCH 04/13] fix(cli): unpack load from default when importing --- @commitlint/cli/src/cli.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/@commitlint/cli/src/cli.js b/@commitlint/cli/src/cli.js index cad49f191f..366b3fcdad 100755 --- a/@commitlint/cli/src/cli.js +++ b/@commitlint/cli/src/cli.js @@ -1,7 +1,8 @@ #!/usr/bin/env node require('babel-polyfill'); // eslint-disable-line import/no-unassigned-import -const load = require('@commitlint/load'); +// fix: commitlint load is ported to typescript, until this one is ported we need to unpack it +const {default: load} = require('@commitlint/load'); const lint = require('@commitlint/lint'); const read = require('@commitlint/read'); const meow = require('meow'); From 87c66f791734cdc4acd36efdc43909c0f15926cb Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sun, 22 Sep 2019 12:12:46 +1000 Subject: [PATCH 05/13] refactor(load): beef up typings --- .editorconfig | 2 +- @commitlint/cli/src/cli.js | 2 +- @commitlint/ensure/src/case.ts | 2 +- @commitlint/ensure/src/index.ts | 3 +- @commitlint/execute-rule/src/index.ts | 2 +- .../load/fixtures/basic/commitlint.config.js | 2 +- @commitlint/load/src/index.serial-test.ts | 20 -- @commitlint/load/src/index.test.ts | 25 +-- @commitlint/load/src/index.ts | 182 ++++++++++++++---- tsconfig.shared.json | 3 +- 10 files changed, 168 insertions(+), 75 deletions(-) delete mode 100644 @commitlint/load/src/index.serial-test.ts 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/cli/src/cli.js b/@commitlint/cli/src/cli.js index 0e21fd94c6..0a2ac830ea 100755 --- a/@commitlint/cli/src/cli.js +++ b/@commitlint/cli/src/cli.js @@ -1,7 +1,7 @@ #!/usr/bin/env node require('babel-polyfill'); // eslint-disable-line import/no-unassigned-import -const load = require('@commitlint/load'); +import load from '@commitlint/load'; const lint = require('@commitlint/lint'); const read = require('@commitlint/read'); const meow = require('meow'); diff --git a/@commitlint/ensure/src/case.ts b/@commitlint/ensure/src/case.ts index 909e73aa68..40d1dde568 100644 --- a/@commitlint/ensure/src/case.ts +++ b/@commitlint/ensure/src/case.ts @@ -2,7 +2,7 @@ import * as _ from 'lodash'; export default ensureCase; -type TargetCaseType = +export type TargetCaseType = | 'camel-case' | 'kebab-case' | 'snake-case' diff --git a/@commitlint/ensure/src/index.ts b/@commitlint/ensure/src/index.ts index 982e128525..bc3a7504c5 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'; @@ -6,5 +6,6 @@ import minLength from './min-length'; import notEmpty from './not-empty'; export {ensureCase as case}; +export {TargetCaseType}; export {ensureEnum as enum}; export {maxLength, maxLineLength, minLength, notEmpty}; 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/src/index.serial-test.ts b/@commitlint/load/src/index.serial-test.ts deleted file mode 100644 index 21ecc55f5d..0000000000 --- a/@commitlint/load/src/index.serial-test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import test from 'ava'; - -const {fix} = require('@commitlint/test'); - -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.ts b/@commitlint/load/src/index.test.ts index e9f5f7fbcd..2fa747f76d 100644 --- a/@commitlint/load/src/index.test.ts +++ b/@commitlint/load/src/index.test.ts @@ -5,10 +5,6 @@ const {fix, git} = require('@commitlint/test'); import load from '.'; -const proxyquire = require('proxyquire') - .noCallThru() - .noPreserveCache(); - const fixture = (name: string) => path.resolve(__dirname, '../fixtures', name); test('extends-empty should have no rules', async () => { @@ -20,25 +16,31 @@ test('extends-empty should have no rules', async () => { test('uses seed as configured', async () => { const cwd = await git.bootstrap(fixture('extends-empty')); - const actual = await load({rules: {foo: 'bar'}}, {cwd}); + const rules = {'body-case': [1, 'never', 'camel-case'] as any}; + + const actual = await load({rules}, {cwd}); - expect(actual.rules.foo).toBe('bar'); + 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 git.bootstrap(fixture('specify-config-file')); - const actual = await load({}, {cwd, file}); + const rules = {'body-case': [1, 'never', 'camel-case'] as any}; - expect(actual.rules.foo).toBe('bar'); + 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 git.bootstrap(fixture('specify-config-file')); const file = path.resolve(cwd, 'config/commitlint.config.js'); - const actual = await load({}, {cwd: process.cwd(), file}); + const rules = {'body-case': [1, 'never', 'camel-case'] as any}; + + const actual = await load({rules}, {cwd: process.cwd(), file}); - expect(actual.rules.foo).toBe('bar'); + expect(actual.rules['body-case']).toStrictEqual([1, 'never', 'camel-case']); }); // test('plugins should be loaded from seed', async () => { @@ -330,10 +332,11 @@ test('returns formatter name when unable to resolve from config directory', asyn test('does not mutate config module reference', async () => { const file = 'config/commitlint.config.js'; const cwd = await git.bootstrap(fixture('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({arbitraryField: true}, {cwd, file}); + await load({rules}, {cwd, file}); const after = JSON.stringify(require(configPath)); expect(after).toBe(before); diff --git a/@commitlint/load/src/index.ts b/@commitlint/load/src/index.ts index fc066a5cab..f2aed1f214 100644 --- a/@commitlint/load/src/index.ts +++ b/@commitlint/load/src/index.ts @@ -1,13 +1,124 @@ import path from 'path'; import executeRule from '@commitlint/execute-rule'; import resolveExtends from '@commitlint/resolve-extends'; +import {TargetCaseType} from '@commitlint/ensure'; import cosmiconfig, {CosmiconfigResult} from 'cosmiconfig'; import {toPairs, merge, mergeWith, pick} from 'lodash'; import resolveFrom from 'resolve-from'; import loadPlugin from './utils/loadPlugin'; -const w = (a: any, b: any) => (Array.isArray(b) ? b : undefined); -const valid = (input: any) => +export interface LoadOptions { + cwd?: string; + file?: string; +} + +export enum RuleSeverity { + Warning = 1, + Error = 2 +} + +export type RuleApplication = 'always' | 'never'; + +export type RuleConfigTuple = ReadonlyArray< + T extends void + ? [RuleSeverity, RuleApplication] + : [RuleSeverity, RuleApplication, T] +>; + +export enum RuleConfigQuality { + User, + Qualified +} + +export type RuleConfig< + V = RuleConfigQuality.Qualified, + T = void +> = V extends false + ? RuleConfigTuple + : + | (() => RuleConfigTuple) + | (() => RuleConfigTuple>) + | RuleConfigTuple; + +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?: any[]; +} + +export type QualifiedRules = Partial>; + +export interface QualifiedConfig { + extends: string[]; + formatter: unknown; + rules: Partial; + parserPreset: ParserPreset; + ignores: ((commit: string) => boolean)[]; + defaultIgnores: boolean; + plugins: any[]; +} + +export interface ParserPreset { + name: string; + path: string; + parserOpts?: unknown; +} + +const w = (_: unknown, b: ArrayLike | null | undefined | false) => + Array.isArray(b) ? b : undefined; +const valid = (input: unknown): UserConfig => pick( input, 'extends', @@ -19,13 +130,17 @@ const valid = (input: any) => 'defaultIgnores' ); -export default async (seed: any = {}, options: any = {cwd: process.cwd()}) => { - const loaded = await loadConfig(options.cwd, options.file); - const base = - loaded && loaded.filepath ? path.dirname(loaded.filepath) : options.cwd; +export default async ( + 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 = valid(merge({}, loaded ? loaded.config : null, seed)); + const opts = merge( {extends: [], rules: {}, formatter: '@commitlint/format'}, pick(config, 'extends', 'plugins', 'ignores', 'defaultIgnores') @@ -50,13 +165,17 @@ export default async (seed: any = {}, options: any = {cwd: process.cwd()}) => { }); const preset = valid(mergeWith(extended, config, w)); + config.extends = Array.isArray(config.extends) ? config.extends : []; + // Await parser-preset if applicable if ( typeof preset.parserPreset === 'object' && - typeof preset.parserPreset.parserOpts === 'object' && - typeof preset.parserPreset.parserOpts.then === 'function' + preset.parserPreset !== null && + typeof (preset.parserPreset as any).parserOpts === 'object' && + (preset.parserPreset as any) !== null && + typeof (preset.parserPreset as any).parserOpts.then === 'function' ) { - preset.parserPreset.parserOpts = (await preset.parserPreset + (preset.parserPreset as any).parserOpts = (await (preset.parserPreset as any) .parserOpts).parserOpts; } @@ -67,41 +186,30 @@ export default async (seed: any = {}, options: any = {cwd: process.cwd()}) => { } // resolve plugins - preset.plugins = {}; - if (config.plugins && config.plugins.length) { + if (Array.isArray(config.plugins)) { config.plugins.forEach((pluginKey: string) => { 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 as any)[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 as any; - (registry as any)[key] = value; - return registry; - }, {}) - ]; - }) - ); - - // Merge executed config keys into preset - return executed.reduce((registry, item) => { - const [key, value] = item; + 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; - }, preset); + }, {}); + + return { + extends: preset.extends!, + formatter: preset.formatter!, + parserPreset: preset.parserPreset! as ParserPreset, + ignores: preset.ignores!, + defaultIgnores: preset.defaultIgnores!, + plugins: preset.plugins!, + rules: qualifiedRules + }; }; async function loadConfig( 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 +} From 0f8a4dbe52055b1a90fb75311221566774778586 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Sun, 22 Sep 2019 13:03:16 +1000 Subject: [PATCH 06/13] fix(load): repair botched merge --- @commitlint/load/package.json | 1 + @commitlint/load/src/index.ts | 182 ++++++++++++++++++++++++++------- @commitlint/load/tsconfig.json | 31 +++--- yarn.lock | 9 ++ 4 files changed, 173 insertions(+), 50 deletions(-) diff --git a/@commitlint/load/package.json b/@commitlint/load/package.json index 8b2c6ed6ff..a3f0073e14 100644 --- a/@commitlint/load/package.json +++ b/@commitlint/load/package.json @@ -42,6 +42,7 @@ "typescript": "3.5.3" }, "dependencies": { + "@commitlint/ensure": "^8.2.0", "@commitlint/execute-rule": "^8.2.0", "@commitlint/resolve-extends": "^8.2.0", "chalk": "2.4.2", diff --git a/@commitlint/load/src/index.ts b/@commitlint/load/src/index.ts index fc066a5cab..f2aed1f214 100644 --- a/@commitlint/load/src/index.ts +++ b/@commitlint/load/src/index.ts @@ -1,13 +1,124 @@ import path from 'path'; import executeRule from '@commitlint/execute-rule'; import resolveExtends from '@commitlint/resolve-extends'; +import {TargetCaseType} from '@commitlint/ensure'; import cosmiconfig, {CosmiconfigResult} from 'cosmiconfig'; import {toPairs, merge, mergeWith, pick} from 'lodash'; import resolveFrom from 'resolve-from'; import loadPlugin from './utils/loadPlugin'; -const w = (a: any, b: any) => (Array.isArray(b) ? b : undefined); -const valid = (input: any) => +export interface LoadOptions { + cwd?: string; + file?: string; +} + +export enum RuleSeverity { + Warning = 1, + Error = 2 +} + +export type RuleApplication = 'always' | 'never'; + +export type RuleConfigTuple = ReadonlyArray< + T extends void + ? [RuleSeverity, RuleApplication] + : [RuleSeverity, RuleApplication, T] +>; + +export enum RuleConfigQuality { + User, + Qualified +} + +export type RuleConfig< + V = RuleConfigQuality.Qualified, + T = void +> = V extends false + ? RuleConfigTuple + : + | (() => RuleConfigTuple) + | (() => RuleConfigTuple>) + | RuleConfigTuple; + +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?: any[]; +} + +export type QualifiedRules = Partial>; + +export interface QualifiedConfig { + extends: string[]; + formatter: unknown; + rules: Partial; + parserPreset: ParserPreset; + ignores: ((commit: string) => boolean)[]; + defaultIgnores: boolean; + plugins: any[]; +} + +export interface ParserPreset { + name: string; + path: string; + parserOpts?: unknown; +} + +const w = (_: unknown, b: ArrayLike | null | undefined | false) => + Array.isArray(b) ? b : undefined; +const valid = (input: unknown): UserConfig => pick( input, 'extends', @@ -19,13 +130,17 @@ const valid = (input: any) => 'defaultIgnores' ); -export default async (seed: any = {}, options: any = {cwd: process.cwd()}) => { - const loaded = await loadConfig(options.cwd, options.file); - const base = - loaded && loaded.filepath ? path.dirname(loaded.filepath) : options.cwd; +export default async ( + 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 = valid(merge({}, loaded ? loaded.config : null, seed)); + const opts = merge( {extends: [], rules: {}, formatter: '@commitlint/format'}, pick(config, 'extends', 'plugins', 'ignores', 'defaultIgnores') @@ -50,13 +165,17 @@ export default async (seed: any = {}, options: any = {cwd: process.cwd()}) => { }); const preset = valid(mergeWith(extended, config, w)); + config.extends = Array.isArray(config.extends) ? config.extends : []; + // Await parser-preset if applicable if ( typeof preset.parserPreset === 'object' && - typeof preset.parserPreset.parserOpts === 'object' && - typeof preset.parserPreset.parserOpts.then === 'function' + preset.parserPreset !== null && + typeof (preset.parserPreset as any).parserOpts === 'object' && + (preset.parserPreset as any) !== null && + typeof (preset.parserPreset as any).parserOpts.then === 'function' ) { - preset.parserPreset.parserOpts = (await preset.parserPreset + (preset.parserPreset as any).parserOpts = (await (preset.parserPreset as any) .parserOpts).parserOpts; } @@ -67,41 +186,30 @@ export default async (seed: any = {}, options: any = {cwd: process.cwd()}) => { } // resolve plugins - preset.plugins = {}; - if (config.plugins && config.plugins.length) { + if (Array.isArray(config.plugins)) { config.plugins.forEach((pluginKey: string) => { 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 as any)[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 as any; - (registry as any)[key] = value; - return registry; - }, {}) - ]; - }) - ); - - // Merge executed config keys into preset - return executed.reduce((registry, item) => { - const [key, value] = item; + 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; - }, preset); + }, {}); + + return { + extends: preset.extends!, + formatter: preset.formatter!, + parserPreset: preset.parserPreset! as ParserPreset, + ignores: preset.ignores!, + defaultIgnores: preset.defaultIgnores!, + plugins: preset.plugins!, + rules: qualifiedRules + }; }; async function loadConfig( diff --git a/@commitlint/load/tsconfig.json b/@commitlint/load/tsconfig.json index f4a57643f0..483777540b 100644 --- a/@commitlint/load/tsconfig.json +++ b/@commitlint/load/tsconfig.json @@ -1,15 +1,20 @@ { - "extends": "../../tsconfig.shared.json", - "compilerOptions": { - "composite": true, - "rootDir": "./src", - "outDir": "./lib" - }, - "include": [ - "./src" - ], - "exclude": [ - "./src/**/*.test.ts", - "./lib/**/*" - ] + "extends": "../../tsconfig.shared.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": [ + "./src" + ], + "exclude": [ + "./src/**/*.test.ts", + "./lib/**/*" + ], + "references": [ + { "path": "../ensure" }, + { "path": "../execute-rule" }, + { "path": "../resolve-extends" } + ] } diff --git a/yarn.lock b/yarn.lock index f919f1234a..f68f6d2aa4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -203,6 +203,15 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@commitlint/test@8.0.0": + version "8.0.0" + resolved "https://registry.npmjs.org/@commitlint/test/-/test-8.0.0.tgz#a0609c87e0fce141af1653894a66e1c4c39b5b10" + integrity sha512-OX+K1vn0Mq805yWR6brqUr0QG4zcV4xYjrw5bDlFIM9hE+Nt7knJ4w6ooAcwMIHL7tlcsKHfIdKtNJBZAewpOA== + dependencies: + "@marionebl/sander" "0.6.1" + execa "0.9.0" + pkg-dir "2.0.0" + "@concordance/react@^1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@concordance/react/-/react-1.0.0.tgz#fcf3cad020e5121bfd1c61d05bc3516aac25f734" From 54e16bc0de0eeec48a72690772f2f8ba0f1e846f Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Tue, 28 Jan 2020 08:07:06 +1100 Subject: [PATCH 07/13] test: port load tests from ava to jest Co-authored-by: Armano --- @commitlint/load/src/utils/loadPlugin.test.ts | 107 ++++++++---------- @commitlint/load/src/utils/loadPlugin.ts | 8 +- jest.config.js | 3 +- 3 files changed, 53 insertions(+), 65 deletions(-) diff --git a/@commitlint/load/src/utils/loadPlugin.test.ts b/@commitlint/load/src/utils/loadPlugin.test.ts index e4e188732a..49f074b3c7 100644 --- a/@commitlint/load/src/utils/loadPlugin.test.ts +++ b/@commitlint/load/src/utils/loadPlugin.test.ts @@ -1,80 +1,65 @@ -import test from 'ava'; -const proxyquire = require('proxyquire') - .noCallThru() - .noPreserveCache(); +import loadPlugin from './loadPlugin'; -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 - }; +jest.mock('commitlint-plugin-example', () => ({example: true}), { + virtual: true }); -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); +jest.mock('@scope/commitlint-plugin-example', () => ({scope: true}), { + virtual: true }); -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 load a plugin when referenced by short name', () => { + const plugins = loadPlugin({}, 'example'); + expect(plugins['example']).toBe(require('commitlint-plugin-example')); }); -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 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 doesn't exist", t => { - const {stubbedLoadPlugin, plugins} = t.context.data; - t.throws(() => { - stubbedLoadPlugin(plugins, 'nonexistentplugin'); - }, /Failed to load plugin/u); +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 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 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 long name', t => { - const {stubbedLoadPlugin, plugins, scopedPlugin} = t.context.data; - stubbedLoadPlugin(plugins, '@scope/commitlint-plugin-example'); - t.is(plugins['@scope/example'], scopedPlugin); +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", 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 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", t => { - const {stubbedLoadPlugin, plugins} = t.context.data; - stubbedLoadPlugin(plugins, '@scope/commitlint-plugin-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", () => { + const plugins = loadPlugin({}, '@scope/commitlint-plugin-example'); + expect(plugins['example']).toBe(undefined); }); diff --git a/@commitlint/load/src/utils/loadPlugin.ts b/@commitlint/load/src/utils/loadPlugin.ts index f693eab9c3..09110cd234 100644 --- a/@commitlint/load/src/utils/loadPlugin.ts +++ b/@commitlint/load/src/utils/loadPlugin.ts @@ -3,11 +3,13 @@ import chalk from 'chalk'; import {normalizePackageName, getShorthandName} from './pluginNaming'; import {WhitespacePluginError, MissingPluginError} from './pluginErrors'; +export type PluginRecords = Record; + export default function loadPlugin( - plugins: any, + plugins: PluginRecords, pluginName: string, debug: boolean = false -) { +): PluginRecords { const longName = normalizePackageName(pluginName); const shortName = getShorthandName(longName); let plugin = null; @@ -66,4 +68,6 @@ export default function loadPlugin( plugins[pluginKey] = plugin; } + + return plugins; } diff --git a/jest.config.js b/jest.config.js index 28a58f4c92..dee037f627 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,7 +4,6 @@ module.exports = { testRegex: undefined, testMatch: [ '**/*.test.ts?(x)', - '**/@commitlint/read/src/*.test.js?(x)', - '**/@commitlint/cli/src/*.test.js?(x)' + '**/@commitlint/{read,cli,load}/src/*.test.js?(x)' ] }; From f254eeca5c972482780c8490adc9d6b381b2ebbc Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Tue, 28 Jan 2020 08:18:46 +1100 Subject: [PATCH 08/13] fix: propagate tightened types --- @commitlint/load/src/index.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/@commitlint/load/src/index.ts b/@commitlint/load/src/index.ts index d18d44f186..e837ee4bae 100644 --- a/@commitlint/load/src/index.ts +++ b/@commitlint/load/src/index.ts @@ -5,7 +5,7 @@ import {TargetCaseType} from '@commitlint/ensure'; import cosmiconfig, {CosmiconfigResult} from 'cosmiconfig'; import {toPairs, merge, mergeWith, pick, startsWith} from 'lodash'; import resolveFrom from 'resolve-from'; -import loadPlugin from './utils/loadPlugin'; +import loadPlugin, {PluginRecords} from './utils/loadPlugin'; export interface LoadOptions { cwd?: string; @@ -95,7 +95,17 @@ export interface UserConfig { parserPreset?: string | ParserPreset; ignores?: ((commit: string) => boolean)[]; defaultIgnores?: boolean; - plugins?: any[]; + 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>; @@ -107,7 +117,7 @@ export interface QualifiedConfig { parserPreset: ParserPreset; ignores: ((commit: string) => boolean)[]; defaultIgnores: boolean; - plugins: any[]; + plugins: PluginRecords; } export interface ParserPreset { @@ -164,7 +174,10 @@ export default async ( parserPreset: config.parserPreset }); - const preset = valid(mergeWith(extended, config, w)); + const preset = (valid( + 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 : []; From c9a1f2ccc5658b9485a72e981cfbb0444793da43 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Mon, 3 Feb 2020 18:46:17 +1100 Subject: [PATCH 09/13] refactor: move out helper functions --- @commitlint/load/package.json | 4 +- @commitlint/load/src/index.serial.test.ts | 20 -- @commitlint/load/src/index.ts | 289 ------------------ .../load/src/{index.test.ts => load.test.ts} | 3 +- @commitlint/load/src/load.ts | 107 +++++++ @commitlint/load/src/types.ts | 123 ++++++++ @commitlint/load/src/utils/load-config.ts | 21 ++ .../load/src/utils/load-parser-opts.ts | 50 +++ @commitlint/load/src/utils/loadPlugin.ts | 3 +- @commitlint/load/src/utils/pick-config.ts | 14 + 10 files changed, 320 insertions(+), 314 deletions(-) delete mode 100644 @commitlint/load/src/index.serial.test.ts delete mode 100644 @commitlint/load/src/index.ts rename @commitlint/load/src/{index.test.ts => load.test.ts} (99%) create mode 100644 @commitlint/load/src/load.ts create mode 100644 @commitlint/load/src/types.ts create mode 100644 @commitlint/load/src/utils/load-config.ts create mode 100644 @commitlint/load/src/utils/load-parser-opts.ts create mode 100644 @commitlint/load/src/utils/pick-config.ts diff --git a/@commitlint/load/package.json b/@commitlint/load/package.json index ce269e43bb..bc697efd0b 100644 --- a/@commitlint/load/package.json +++ b/@commitlint/load/package.json @@ -2,8 +2,8 @@ "name": "@commitlint/load", "version": "8.3.5", "description": "Load shared commitlint configuration", - "main": "lib/index.js", - "types": "lib/index.d.ts", + "main": "lib/load.js", + "types": "lib/load.d.ts", "files": [ "lib/" ], diff --git a/@commitlint/load/src/index.serial.test.ts b/@commitlint/load/src/index.serial.test.ts deleted file mode 100644 index 188590f399..0000000000 --- a/@commitlint/load/src/index.serial.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as path from 'path'; -import {fix} from '@commitlint/test'; -import load from '.'; - -const fixBootstrap = (name: string) => fix.bootstrap(name, __dirname); - -test('default cwd option to process.cwd()', async () => { - const cwd = await fixBootstrap('fixtures/basic'); - const before = process.cwd(); - process.chdir(cwd); - - try { - const actual = await load(); - expect(actual.rules['body-case']).toBeTruthy(); - } catch (err) { - throw err; - } finally { - process.chdir(before); - } -}); diff --git a/@commitlint/load/src/index.ts b/@commitlint/load/src/index.ts deleted file mode 100644 index e837ee4bae..0000000000 --- a/@commitlint/load/src/index.ts +++ /dev/null @@ -1,289 +0,0 @@ -import path from 'path'; -import executeRule from '@commitlint/execute-rule'; -import resolveExtends from '@commitlint/resolve-extends'; -import {TargetCaseType} from '@commitlint/ensure'; -import cosmiconfig, {CosmiconfigResult} from 'cosmiconfig'; -import {toPairs, merge, mergeWith, pick, startsWith} from 'lodash'; -import resolveFrom from 'resolve-from'; -import loadPlugin, {PluginRecords} from './utils/loadPlugin'; - -export interface LoadOptions { - cwd?: string; - file?: string; -} - -export enum RuleSeverity { - Warning = 1, - Error = 2 -} - -export type RuleApplication = 'always' | 'never'; - -export type RuleConfigTuple = ReadonlyArray< - T extends void - ? [RuleSeverity, RuleApplication] - : [RuleSeverity, RuleApplication, T] ->; - -export enum RuleConfigQuality { - User, - Qualified -} - -export type RuleConfig< - V = RuleConfigQuality.Qualified, - T = void -> = V extends false - ? RuleConfigTuple - : - | (() => RuleConfigTuple) - | (() => RuleConfigTuple>) - | RuleConfigTuple; - -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; -} - -const w = (_: unknown, b: ArrayLike | null | undefined | false) => - Array.isArray(b) ? b : undefined; -const valid = (input: unknown): UserConfig => - pick( - input, - 'extends', - 'rules', - 'plugins', - 'parserPreset', - 'formatter', - 'ignores', - 'defaultIgnores' - ); - -export default async ( - 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 = valid(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 = (valid( - 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 - }; -}; - -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; -} - -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/index.test.ts b/@commitlint/load/src/load.test.ts similarity index 99% rename from @commitlint/load/src/index.test.ts rename to @commitlint/load/src/load.test.ts index bfbb265ced..4caeb1cf9e 100644 --- a/@commitlint/load/src/index.test.ts +++ b/@commitlint/load/src/load.test.ts @@ -11,7 +11,8 @@ import execa from 'execa'; import resolveFrom from 'resolve-from'; import {fix, git, npm} from '@commitlint/test'; -import load from '.'; +import load from './load'; +import {RuleSeverity} from './types'; const fixBootstrap = (name: string) => fix.bootstrap(name, __dirname); const gitBootstrap = (name: string) => git.bootstrap(name, __dirname); 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.ts b/@commitlint/load/src/utils/loadPlugin.ts index 09110cd234..724f2f7ebc 100644 --- a/@commitlint/load/src/utils/loadPlugin.ts +++ b/@commitlint/load/src/utils/loadPlugin.ts @@ -2,8 +2,7 @@ import path from 'path'; import chalk from 'chalk'; import {normalizePackageName, getShorthandName} from './pluginNaming'; import {WhitespacePluginError, MissingPluginError} from './pluginErrors'; - -export type PluginRecords = Record; +import {PluginRecords} from '../types'; export default function loadPlugin( plugins: PluginRecords, 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' + ); From 8ba437a5b9f9c102dcd998f71e0381d15ee65d15 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Mon, 3 Feb 2020 18:46:48 +1100 Subject: [PATCH 10/13] test: match prompt-cli tests correctly --- jest.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index 48c8f8fc5e..61916467a2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,6 +4,7 @@ module.exports = { testRegex: undefined, testMatch: [ '**/*.test.ts?(x)', - '**/@commitlint/{lint,read,travis-cli,cli,prompt-cli,load}/src/*.test.js?(x)' + '**/@commitlint/{lint,read,travis-cli,cli,load}/src/*.test.js?(x)', + '**/@commitlint/prompt-cli/*.test.js?(x)' ] }; From 51c8be22ab2d096aff3a89a08b808a32452d3098 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Mon, 3 Feb 2020 18:58:44 +1100 Subject: [PATCH 11/13] fix: add missing dependency on @commitlint/rules --- @commitlint/load/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/@commitlint/load/package.json b/@commitlint/load/package.json index bc697efd0b..906814f897 100644 --- a/@commitlint/load/package.json +++ b/@commitlint/load/package.json @@ -45,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", From 0411e14fa75d547a1985ab4be84540e12c47d81d Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Mon, 3 Feb 2020 19:02:32 +1100 Subject: [PATCH 12/13] fix: add missing reference --- @commitlint/load/tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/@commitlint/load/tsconfig.json b/@commitlint/load/tsconfig.json index 483777540b..955cf565b8 100644 --- a/@commitlint/load/tsconfig.json +++ b/@commitlint/load/tsconfig.json @@ -13,8 +13,8 @@ "./lib/**/*" ], "references": [ - { "path": "../ensure" }, { "path": "../execute-rule" }, - { "path": "../resolve-extends" } + { "path": "../resolve-extends" }, + { "path": "../rules" } ] } From cde50d22da75f10fbbddeff47665899166fd5191 Mon Sep 17 00:00:00 2001 From: Mario Nebl Date: Mon, 3 Feb 2020 19:13:28 +1100 Subject: [PATCH 13/13] fix: add missing references --- @commitlint/rules/package.json | 1 + @commitlint/rules/tsconfig.json | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/@commitlint/rules/package.json b/@commitlint/rules/package.json index 9eab1b36dc..484b37bd46 100644 --- a/@commitlint/rules/package.json +++ b/@commitlint/rules/package.json @@ -50,6 +50,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" } + ] }