Skip to content

Commit

Permalink
Add support for JS configuration via --config (fixes #85).
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidAnson committed May 3, 2020
1 parent 794167a commit a1f9a15
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 16 deletions.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
34 changes: 24 additions & 10 deletions markdownlint.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
Expand All @@ -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);

This comment has been minimized.

Copy link
@victoriadrake

victoriadrake Jul 27, 2020

This seems to fail silently (#105)

This comment has been minimized.

Copy link
@DavidAnson

DavidAnson Jul 27, 2020

Author Collaborator

If you can please give an example or steps to reproduce this, it would help understand what’s happening.

This comment has been minimized.

Copy link
@victoriadrake

victoriadrake Jul 28, 2020

Absolutely. I’ve replied in the #105 issue.

This comment has been minimized.

Copy link
@DavidAnson

DavidAnson Jul 28, 2020

Author Collaborator

Great detail there, thank you!

}
}

Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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)
};
});
Expand Down Expand Up @@ -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, []);
Expand All @@ -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
Expand All @@ -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;
}
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -72,7 +73,8 @@
"ava": {
"files": [
"test/**/*.js",
"!test/custom-rules/**/*.js"
"!test/custom-rules/**/*.js",
"!test/md043-config.js"
],
"failFast": true
}
Expand Down
14 changes: 14 additions & 0 deletions test/md043-config.js
Original file line number Diff line number Diff line change
@@ -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'
]
}
};
8 changes: 8 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit a1f9a15

Please sign in to comment.