UNPKG

7.86 kBJavaScriptView Raw
1#!/usr/bin/env node
2require('babel-polyfill'); // eslint-disable-line import/no-unassigned-import
3
4const load = require('@commitlint/load');
5const lint = require('@commitlint/lint');
6const read = require('@commitlint/read');
7const meow = require('meow');
8const {merge, pick} = require('lodash');
9const stdin = require('get-stdin');
10const resolveFrom = require('resolve-from');
11const resolveGlobal = require('resolve-global');
12
13const pkg = require('../package');
14const help = require('./help');
15
16const flags = {
17 color: {
18 alias: 'c',
19 default: true,
20 description: 'toggle colored output',
21 type: 'boolean'
22 },
23 config: {
24 alias: 'g',
25 default: null,
26 description: 'path to the config file',
27 type: 'string'
28 },
29 cwd: {
30 alias: 'd',
31 default: process.cwd(),
32 description: 'directory to execute in',
33 type: 'string'
34 },
35 edit: {
36 alias: 'e',
37 default: false,
38 description:
39 'read last commit message from the specified file or fallbacks to ./.git/COMMIT_EDITMSG',
40 type: 'string'
41 },
42 env: {
43 alias: 'E',
44 default: null,
45 description:
46 'check message in the file at path given by environment variable value',
47 type: 'string'
48 },
49 extends: {
50 alias: 'x',
51 description: 'array of shareable configurations to extend',
52 type: 'string'
53 },
54 help: {
55 alias: 'h',
56 type: 'boolean',
57 description: 'display this help message'
58 },
59 from: {
60 alias: 'f',
61 default: null,
62 description: 'lower end of the commit range to lint; applies if edit=false',
63 type: 'string'
64 },
65 format: {
66 alias: 'o',
67 default: null,
68 description: 'output format of the results',
69 type: 'string'
70 },
71 'parser-preset': {
72 alias: 'p',
73 description: 'configuration preset to use for conventional-commits-parser',
74 type: 'string'
75 },
76 quiet: {
77 alias: 'q',
78 default: false,
79 description: 'toggle console output',
80 type: 'boolean'
81 },
82 to: {
83 alias: 't',
84 default: null,
85 description: 'upper end of the commit range to lint; applies if edit=false',
86 type: 'string'
87 },
88 version: {
89 alias: 'v',
90 type: 'boolean',
91 description: 'display version information'
92 }
93};
94
95const cli = meow({
96 description: `${pkg.name}@${pkg.version} - ${pkg.description}`,
97 flags,
98 help: `[input] reads from stdin if --edit, --env, --from and --to are omitted\n${help(
99 flags
100 )}`,
101 unknown(arg) {
102 throw new Error(`unknown flags: ${arg}`);
103 }
104});
105
106main(cli).catch(err =>
107 setTimeout(() => {
108 if (err.type === pkg.name) {
109 process.exit(1);
110 }
111 throw err;
112 })
113);
114
115async function main(options) {
116 const raw = options.input;
117 const flags = normalizeFlags(options.flags);
118 const fromStdin = checkFromStdin(raw, flags);
119
120 const range = pick(flags, 'edit', 'from', 'to');
121
122 const input = await (fromStdin ? stdin() : read(range, {cwd: flags.cwd}));
123
124 const messages = (Array.isArray(input) ? input : [input])
125 .filter(message => typeof message === 'string')
126 .filter(message => message.trim() !== '')
127 .filter(Boolean);
128
129 if (messages.length === 0 && !checkFromRepository(flags)) {
130 const err = new Error(
131 '[input] is required: supply via stdin, or --env or --edit or --from and --to'
132 );
133 err.type = pkg.name;
134 console.log(`${cli.help}\n`);
135 console.log(err.message);
136 throw err;
137 }
138
139 const loadOpts = {cwd: flags.cwd, file: flags.config};
140 const loaded = await load(getSeed(flags), loadOpts);
141 const parserOpts = selectParserOpts(loaded.parserPreset);
142 const opts = parserOpts ? {parserOpts} : {parserOpts: {}};
143 const format = loadFormatter(loaded, flags);
144
145 // Strip comments if reading from `.git/COMMIT_EDIT_MSG`
146 if (range.edit) {
147 opts.parserOpts.commentChar = '#';
148 }
149
150 const results = await Promise.all(
151 messages.map(message => lint(message, loaded.rules, opts))
152 );
153
154 if (Object.keys(loaded.rules).length === 0) {
155 let input = '';
156
157 if (results.length !== 0) {
158 const originalInput = results[0].input;
159 input = originalInput;
160 }
161
162 results.splice(0, results.length, {
163 valid: false,
164 errors: [
165 {
166 level: 2,
167 valid: false,
168 name: 'empty-rules',
169 message: [
170 'Please add rules to your `commitlint.config.js`',
171 ' - Getting started guide: https://git.io/fpUzJ',
172 ' - Example config: https://git.io/fpUzm'
173 ].join('\n')
174 }
175 ],
176 warnings: [],
177 input
178 });
179 }
180
181 const report = results.reduce(
182 (info, result) => {
183 info.valid = result.valid ? info.valid : false;
184 info.errorCount += result.errors.length;
185 info.warningCount += result.warnings.length;
186 info.results.push(result);
187
188 return info;
189 },
190 {
191 valid: true,
192 errorCount: 0,
193 warningCount: 0,
194 results: []
195 }
196 );
197
198 const output = format(report, {color: flags.color});
199
200 if (!flags.quiet) {
201 console.log(output);
202 }
203
204 if (!report.valid) {
205 const err = new Error(output);
206 err.type = pkg.name;
207 throw err;
208 }
209}
210
211function checkFromStdin(input, flags) {
212 return input.length === 0 && !checkFromRepository(flags);
213}
214
215function checkFromRepository(flags) {
216 return checkFromHistory(flags) || checkFromEdit(flags);
217}
218
219function checkFromEdit(flags) {
220 return Boolean(flags.edit) || flags.env;
221}
222
223function checkFromHistory(flags) {
224 return typeof flags.from === 'string' || typeof flags.to === 'string';
225}
226
227function normalizeFlags(flags) {
228 const edit = getEditValue(flags);
229 return merge({}, flags, {edit, e: edit});
230}
231
232function getEditValue(flags) {
233 if (flags.env) {
234 if (!(flags.env in process.env)) {
235 throw new Error(
236 `Recieved '${
237 flags.env
238 }' as value for -E | --env, but environment variable '${
239 flags.env
240 }' is not available globally`
241 );
242 }
243 return process.env[flags.env];
244 }
245 const {edit} = flags;
246 // If the edit flag is set but empty (i.e '-e') we default
247 // to .git/COMMIT_EDITMSG
248 if (edit === '') {
249 return true;
250 }
251 if (typeof edit === 'boolean') {
252 return edit;
253 }
254 // The recommended method to specify -e with husky was `commitlint -e $HUSKY_GIT_PARAMS`
255 // This does not work properly with win32 systems, where env variable declarations
256 // use a different syntax
257 // See https://github.com/conventional-changelog/commitlint/issues/103 for details
258 // This has been superceded by the `-E GIT_PARAMS` / `-E HUSKY_GIT_PARAMS`
259 const isGitParams = edit === '$GIT_PARAMS' || edit === '%GIT_PARAMS%';
260 const isHuskyParams =
261 edit === '$HUSKY_GIT_PARAMS' || edit === '%HUSKY_GIT_PARAMS%';
262
263 if (isGitParams || isHuskyParams) {
264 console.warn(`Using environment variable syntax (${edit}) in -e |\
265--edit is deprecated. Use '{-E|--env} HUSKY_GIT_PARAMS instead'`);
266
267 if (isGitParams && 'GIT_PARAMS' in process.env) {
268 return process.env.GIT_PARAMS;
269 }
270 if ('HUSKY_GIT_PARAMS' in process.env) {
271 return process.env.HUSKY_GIT_PARAMS;
272 }
273 throw new Error(
274 `Received ${edit} as value for -e | --edit, but GIT_PARAMS or HUSKY_GIT_PARAMS are not available globally.`
275 );
276 }
277 return edit;
278}
279
280function getSeed(seed) {
281 const e = Array.isArray(seed.extends) ? seed.extends : [seed.extends];
282 const n = e.filter(i => typeof i === 'string');
283 return n.length > 0
284 ? {extends: n, parserPreset: seed.parserPreset}
285 : {parserPreset: seed.parserPreset};
286}
287
288function selectParserOpts(parserPreset) {
289 if (typeof parserPreset !== 'object') {
290 return undefined;
291 }
292
293 if (typeof parserPreset.parserOpts !== 'object') {
294 return undefined;
295 }
296
297 return parserPreset.parserOpts;
298}
299
300function loadFormatter(config, flags) {
301 const moduleName = flags.format || config.formatter || '@commitlint/format';
302 const modulePath =
303 resolveFrom.silent(__dirname, moduleName) ||
304 resolveFrom.silent(flags.cwd, moduleName) ||
305 resolveGlobal.silent(moduleName);
306
307 if (modulePath) {
308 return require(modulePath);
309 }
310
311 throw new Error(`Using format ${moduleName}, but cannot find the module.`);
312}
313
314// Catch unhandled rejections globally
315process.on('unhandledRejection', (reason, promise) => {
316 console.log('Unhandled Rejection at: Promise ', promise, ' reason: ', reason);
317 throw reason;
318});