diff --git a/README.md b/README.md index 2a604cc97..d0e9d44dc 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ markdownlint --help -f, --fix fix basic errors (does not work with STDIN) -s, --stdin read from STDIN (does not work with files) -o, --output [outputFile] write issues to file (no console) - -c, --config [configFile] configuration file (JSON, JSONC, or YAML) + -c, --config [configFile] configuration file (JSON, JSONC, JS, or YAML) -i, --ignore [file|directory|glob] file(s) to ignore/exclude -p, --ignore-path [file] path to file with ignore pattern(s) -r, --rules [file|directory|glob|package] custom rule files @@ -83,10 +83,14 @@ The example of configuration file: See [test configuration file][test-config] or [style folder][style-folder] for more examples. -CLI argument `--config` is not mandatory. -If it is not provided, `markdownlint-cli` looks for file `.markdownlint.json`/`.markdownlint.yaml`/`.markdownlint.yml` in current folder, or for file `.markdownlintrc` in current or all upper folders. -The algorithm is described in details on [rc package page][rc-standards]. -If `--config` argument is provided, the file must be valid JSON, JSONC, or YAML. +The CLI argument `--config` is not required. +If it is not provided, `markdownlint-cli` looks for the file `.markdownlint.json`/`.markdownlint.yaml`/`.markdownlint.yml` in current folder, or for the file `.markdownlintrc` in the current or all parent folders. +The algorithm is described in detail on the [`rc` package page][rc-standards]. +If the `--config` argument is provided, the file must be valid JSON, JSONC, JS, or YAML. +JS configuration files contain JavaScript code, must have the `.js` extension, and must export (via `module.exports = ...`) a configuration object of the form shown above. +A JS configuration file may internally `require` one or more npm packages as a way of reusing configuration across projects. + +> JS configuration files must be provided via the `--config` argument; they are not automatically loaded because running untrusted code is a security concern. ## Exit codes diff --git a/markdownlint.js b/markdownlint.js index 671e2bffa..c84ea0884 100755 --- a/markdownlint.js +++ b/markdownlint.js @@ -12,6 +12,7 @@ const markdownlint = require('markdownlint'); const rc = require('rc'); const glob = require('glob'); const minimatch = require('minimatch'); +const minimist = require('minimist'); const pkg = require('./package'); function jsoncParse(text) { @@ -29,10 +30,19 @@ const projectConfigFiles = [ ]; const configFileParsers = [jsoncParse, jsYamlSafeLoad]; const fsOptions = {encoding: 'utf8'}; +const processCwd = process.cwd(); function readConfiguration(args) { - let config = rc('markdownlint', {}); const userConfigFile = args.config; + const jsConfigFile = /\.js$/i.test(userConfigFile); + const rcArgv = minimist(process.argv.slice(2)); + if (jsConfigFile) { + // Prevent rc package from parsing .js config file as INI + delete rcArgv.config; + } + + // Load from well-known config files + let config = rc('markdownlint', {}, rcArgv); for (const projectConfigFile of projectConfigFiles) { try { fs.accessSync(projectConfigFile, fs.R_OK); @@ -43,17 +53,21 @@ function readConfiguration(args) { // Ignore failure } } + // Normally parsing this file is not needed, // because it is already parsed by rc package. // However I have to do it to overwrite configuration // from .markdownlint.{json,yaml,yml}. - if (userConfigFile) { try { - const userConfig = markdownlint.readConfigSync(userConfigFile, configFileParsers); + const userConfig = jsConfigFile ? + // Evaluate .js configuration file as code + require(path.resolve(processCwd, userConfigFile)) : + // Load JSON/YAML configuration as data + markdownlint.readConfigSync(userConfigFile, configFileParsers); config = require('deep-extend')(config, userConfig); } catch (error) { - console.warn('Cannot read or parse config file ' + args.config + ': ' + error.message); + console.warn('Cannot read or parse config file ' + userConfigFile + ': ' + error.message); } } @@ -78,7 +92,7 @@ function prepareFileList(files, fileExtensions, previousResults) { // Directory (file falls through to below) if (previousResults) { const matcher = new minimatch.Minimatch( - path.resolve(process.cwd(), path.join(file, '**', extensionGlobPart)), globOptions); + path.resolve(processCwd, path.join(file, '**', extensionGlobPart)), globOptions); return previousResults.filter(function (fileInfo) { return matcher.match(fileInfo.absolute); }).map(function (fileInfo) { @@ -91,7 +105,7 @@ function prepareFileList(files, fileExtensions, previousResults) { } catch (_) { // Not a directory, not a file, may be a glob if (previousResults) { - const matcher = new minimatch.Minimatch(path.resolve(process.cwd(), file), globOptions); + const matcher = new minimatch.Minimatch(path.resolve(processCwd, file), globOptions); return previousResults.filter(function (fileInfo) { return matcher.match(fileInfo.absolute); }).map(function (fileInfo) { @@ -108,7 +122,7 @@ function prepareFileList(files, fileExtensions, previousResults) { return flatten(files).map(function (file) { return { original: file, - relative: path.relative(process.cwd(), file), + relative: path.relative(processCwd, file), absolute: path.resolve(file) }; }); @@ -171,7 +185,7 @@ program .option('-f, --fix', 'fix basic errors (does not work with STDIN)') .option('-s, --stdin', 'read from STDIN (does not work with files)') .option('-o, --output [outputFile]', 'write issues to file (no console)') - .option('-c, --config [configFile]', 'configuration file (JSON, JSONC, or YAML)') + .option('-c, --config [configFile]', 'configuration file (JSON, JSONC, JS, or YAML)') .option('-i, --ignore [file|directory|glob]', 'file(s) to ignore/exclude', concatArray, []) .option('-p, --ignore-path [file]', 'path to file with ignore pattern(s)') .option('-r, --rules [file|directory|glob|package]', 'custom rule files', concatArray, []); @@ -183,7 +197,7 @@ function tryResolvePath(filepath) { if (path.basename(filepath) === filepath && path.extname(filepath) === '') { // Looks like a package name, resolve it relative to cwd // Get list of directories, where requested module can be. - let paths = Module._nodeModulePaths(process.cwd()); + let paths = Module._nodeModulePaths(processCwd); paths = paths.concat(Module.globalPaths); if (require.resolve.paths) { // Node >= 8.9.0 @@ -194,7 +208,7 @@ function tryResolvePath(filepath) { } // Maybe it is a path to package installed locally - return require.resolve(path.join(process.cwd(), filepath)); + return require.resolve(path.join(processCwd, filepath)); } catch (_) { return filepath; } diff --git a/package.json b/package.json index b860a6557..91b8c6d3e 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "markdownlint": "~0.20.2", "markdownlint-rule-helpers": "~0.9.0", "minimatch": "~3.0.4", + "minimist": "~1.2.5", "rc": "~1.2.7" }, "devDependencies": { @@ -72,7 +73,8 @@ "ava": { "files": [ "test/**/*.js", - "!test/custom-rules/**/*.js" + "!test/custom-rules/**/*.js", + "!test/md043-config.js" ], "failFast": true } diff --git a/test/md043-config.js b/test/md043-config.js new file mode 100644 index 000000000..ec5e44c9c --- /dev/null +++ b/test/md043-config.js @@ -0,0 +1,14 @@ +'use strict'; + +// Export config object directly (as below) +// -OR- +// via require('some-npm-module-that-exports-config') +module.exports = { + MD043: { + headers: [ + '# First', + '## Second', + '### Third' + ] + } +}; diff --git a/test/test.js b/test/test.js index 5bc6e1f95..7b7ecd4ba 100644 --- a/test/test.js +++ b/test/test.js @@ -365,6 +365,14 @@ test('configuration file can be YAML', async t => { t.is(result.stderr, ''); }); +test('configuration file can be JavaScript', async t => { + const result = await execa('../markdownlint.js', + ['--config', 'md043-config.js', 'md043-config.md'], + {stripFinalNewline: false}); + t.is(result.stdout, ''); + t.is(result.stderr, ''); +}); + function getCwdConfigFileTest(extension) { return async t => { try {