UNPKG

10.2 kBJavaScriptView Raw
1#!/usr/bin/env node
2
3'use strict';
4
5const fs = require('fs');
6const path = require('path');
7const Module = require('module');
8const program = require('commander');
9const options = program.opts();
10const differenceWith = require('lodash.differencewith');
11const flatten = require('lodash.flatten');
12const markdownlint = require('markdownlint');
13const rc = require('rc');
14const glob = require('glob');
15const minimatch = require('minimatch');
16const minimist = require('minimist');
17const pkg = require('./package');
18const os = require('os');
19
20function jsoncParse(text) {
21 return JSON.parse(require('jsonc-parser').stripComments(text));
22}
23
24function jsYamlSafeLoad(text) {
25 return require('js-yaml').load(text);
26}
27
28const projectConfigFiles = [
29 '.markdownlint.json',
30 '.markdownlint.yaml',
31 '.markdownlint.yml'
32];
33const configFileParsers = [jsoncParse, jsYamlSafeLoad];
34const fsOptions = {encoding: 'utf8'};
35const processCwd = process.cwd();
36
37function readConfiguration(args) {
38 const userConfigFile = args.config;
39 const jsConfigFile = /\.js$/i.test(userConfigFile);
40 const rcArgv = minimist(process.argv.slice(2));
41 if (jsConfigFile) {
42 // Prevent rc package from parsing .js config file as INI
43 delete rcArgv.config;
44 }
45
46 // Load from well-known config files
47 let config = rc('markdownlint', {}, rcArgv);
48 for (const projectConfigFile of projectConfigFiles) {
49 try {
50 fs.accessSync(projectConfigFile, fs.R_OK);
51 const projectConfig = markdownlint.readConfigSync(projectConfigFile, configFileParsers);
52 config = require('deep-extend')(config, projectConfig);
53 break;
54 } catch {
55 // Ignore failure
56 }
57 }
58
59 // Normally parsing this file is not needed,
60 // because it is already parsed by rc package.
61 // However I have to do it to overwrite configuration
62 // from .markdownlint.{json,yaml,yml}.
63 if (userConfigFile) {
64 try {
65 const userConfig = jsConfigFile ?
66 // Evaluate .js configuration file as code
67 require(path.resolve(processCwd, userConfigFile)) :
68 // Load JSON/YAML configuration as data
69 markdownlint.readConfigSync(userConfigFile, configFileParsers);
70 config = require('deep-extend')(config, userConfig);
71 } catch (error) {
72 console.warn('Cannot read or parse config file ' + userConfigFile + ': ' + error.message);
73 }
74 }
75
76 return config;
77}
78
79function prepareFileList(files, fileExtensions, previousResults) {
80 const globOptions = {
81 dot: Boolean(options.dot),
82 nodir: true
83 };
84 let extensionGlobPart = '*.';
85 if (fileExtensions.length === 1) {
86 // Glob seems not to match patterns like 'foo.{js}'
87 extensionGlobPart += fileExtensions[0];
88 } else {
89 extensionGlobPart += '{' + fileExtensions.join(',') + '}';
90 }
91
92 files = files.map(function (file) {
93 try {
94 if (fs.lstatSync(file).isDirectory()) {
95 // Directory (file falls through to below)
96 if (previousResults) {
97 const matcher = new minimatch.Minimatch(
98 path.resolve(processCwd, path.join(file, '**', extensionGlobPart)), globOptions);
99 return previousResults.filter(function (fileInfo) {
100 return matcher.match(fileInfo.absolute);
101 }).map(function (fileInfo) {
102 return fileInfo.original;
103 });
104 }
105
106 return glob.sync(path.join(file, '**', extensionGlobPart), globOptions);
107 }
108 } catch {
109 // Not a directory, not a file, may be a glob
110 if (previousResults) {
111 const matcher = new minimatch.Minimatch(path.resolve(processCwd, file), globOptions);
112 return previousResults.filter(function (fileInfo) {
113 return matcher.match(fileInfo.absolute);
114 }).map(function (fileInfo) {
115 return fileInfo.original;
116 });
117 }
118
119 return glob.sync(file, globOptions);
120 }
121
122 // File
123 return file;
124 });
125 return flatten(files).map(function (file) {
126 return {
127 original: file,
128 relative: path.relative(processCwd, file),
129 absolute: path.resolve(file)
130 };
131 });
132}
133
134function printResult(lintResult) {
135 const results = flatten(Object.keys(lintResult).map(file => {
136 return lintResult[file].map(result => {
137 return {
138 file: file,
139 lineNumber: result.lineNumber,
140 column: (result.errorRange && result.errorRange[0]) || 0,
141 names: result.ruleNames.join('/'),
142 description: result.ruleDescription +
143 (result.errorDetail ? ' [' + result.errorDetail + ']' : '') +
144 (result.errorContext ? ' [Context: "' + result.errorContext + '"]' : '')
145 };
146 });
147 }));
148 let lintResultString = '';
149 if (results.length > 0) {
150 results.sort((a, b) => {
151 return a.file.localeCompare(b.file) || a.lineNumber - b.lineNumber ||
152 a.names.localeCompare(b.names) || a.description.localeCompare(b.description);
153 });
154 lintResultString = results.map(result => {
155 const {file, lineNumber, column, names, description} = result;
156 const columnText = column ? `:${column}` : '';
157 return `${file}:${lineNumber}${columnText} ${names} ${description}`;
158 }).join('\n');
159 // Note: process.exit(1) will end abruptly, interrupting asynchronous IO
160 // streams (e.g., when the output is being piped). Just set the exit code
161 // and let the program terminate normally.
162 // @see {@link https://nodejs.org/dist/latest-v8.x/docs/api/process.html#process_process_exit_code}
163 // @see {@link https://github.com/igorshubovych/markdownlint-cli/pull/29#issuecomment-343535291}
164 process.exitCode = 1;
165 }
166
167 if (options.output) {
168 lintResultString = lintResultString.length > 0 ?
169 lintResultString + os.EOL :
170 lintResultString;
171 try {
172 fs.writeFileSync(options.output, lintResultString);
173 } catch (error) {
174 console.warn('Cannot write to output file ' + options.output + ': ' + error.message);
175 process.exitCode = 2;
176 }
177 } else if (lintResultString) {
178 console.error(lintResultString);
179 }
180}
181
182function concatArray(item, array) {
183 array.push(item);
184 return array;
185}
186
187program
188 .version(pkg.version)
189 .description(pkg.description)
190 .usage('[options] <files|directories|globs>')
191 .option('-c, --config [configFile]', 'configuration file (JSON, JSONC, JS, or YAML)')
192 .option('-d, --dot', 'include files/folders with a dot (for example `.github`)')
193 .option('-f, --fix', 'fix basic errors (does not work with STDIN)')
194 .option('-i, --ignore [file|directory|glob]', 'file(s) to ignore/exclude', concatArray, [])
195 .option('-o, --output [outputFile]', 'write issues to file (no console)')
196 .option('-p, --ignore-path [file]', 'path to file with ignore pattern(s)')
197 .option('-r, --rules [file|directory|glob|package]', 'custom rule files', concatArray, [])
198 .option('-s, --stdin', 'read from STDIN (does not work with files)');
199
200program.parse(process.argv);
201
202function tryResolvePath(filepath) {
203 try {
204 if (path.basename(filepath) === filepath && path.extname(filepath) === '') {
205 // Looks like a package name, resolve it relative to cwd
206 // Get list of directories, where requested module can be.
207 let paths = Module._nodeModulePaths(processCwd);
208 // eslint-disable-next-line unicorn/prefer-spread
209 paths = paths.concat(Module.globalPaths);
210 if (require.resolve.paths) {
211 // Node >= 8.9.0
212 return require.resolve(filepath, {paths: paths});
213 }
214
215 return Module._resolveFilename(filepath, {paths: paths});
216 }
217
218 // Maybe it is a path to package installed locally
219 return require.resolve(path.join(processCwd, filepath));
220 } catch {
221 return filepath;
222 }
223}
224
225function loadCustomRules(rules) {
226 return flatten(rules.map(function (rule) {
227 try {
228 const resolvedPath = [tryResolvePath(rule)];
229 const fileList = flatten(prepareFileList(resolvedPath, ['js']).map(function (filepath) {
230 return require(filepath.absolute);
231 }));
232 if (fileList.length === 0) {
233 throw new Error('No such rule');
234 }
235
236 return fileList;
237 } catch (error) {
238 console.error('Cannot load custom rule ' + rule + ': ' + error.message);
239 process.exit(3);
240 }
241 }));
242}
243
244let ignorePath = '.markdownlintignore';
245let {existsSync} = fs;
246if (options.ignorePath) {
247 ignorePath = options.ignorePath;
248 existsSync = () => true;
249}
250
251let ignoreFilter = () => true;
252if (existsSync(ignorePath)) {
253 const ignoreText = fs.readFileSync(ignorePath, fsOptions);
254 const ignore = require('ignore');
255 const ignoreInstance = ignore().add(ignoreText);
256 ignoreFilter = fileInfo => !ignoreInstance.ignores(fileInfo.relative);
257}
258
259const files = prepareFileList(program.args, ['md', 'markdown'])
260 .filter(value => ignoreFilter(value));
261const ignores = prepareFileList(options.ignore, ['md', 'markdown'], files);
262const customRules = loadCustomRules(options.rules);
263const diff = differenceWith(files, ignores, function (a, b) {
264 return a.absolute === b.absolute;
265}).map(function (paths) {
266 return paths.original;
267});
268
269function lintAndPrint(stdin, files) {
270 files = files || [];
271 const config = readConfiguration(program);
272 const lintOptions = {
273 config,
274 customRules,
275 files
276 };
277 if (stdin) {
278 lintOptions.strings = {
279 stdin
280 };
281 }
282
283 if (options.fix) {
284 const fixOptions = {
285 ...lintOptions,
286 resultVersion: 3
287 };
288 const markdownlintRuleHelpers = require('markdownlint-rule-helpers');
289 for (const file of files) {
290 fixOptions.files = [file];
291 const fixResult = markdownlint.sync(fixOptions);
292 const fixes = fixResult[file].filter(error => error.fixInfo);
293 if (fixes.length > 0) {
294 const originalText = fs.readFileSync(file, fsOptions);
295 const fixedText = markdownlintRuleHelpers.applyFixes(originalText, fixes);
296 if (originalText !== fixedText) {
297 fs.writeFileSync(file, fixedText, fsOptions);
298 }
299 }
300 }
301 }
302
303 const lintResult = markdownlint.sync(lintOptions);
304 printResult(lintResult);
305}
306
307if ((files.length > 0) && !options.stdin) {
308 lintAndPrint(null, diff);
309} else if ((files.length === 0) && options.stdin && !options.fix) {
310 const getStdin = require('get-stdin');
311 getStdin().then(lintAndPrint);
312} else {
313 program.help();
314}