markdownlint-cli2 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020-2021 David Anson
3
+ Copyright (c) 2020-2022 David Anson
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -20,6 +20,9 @@ As a development dependency of the current package:
20
20
  npm install markdownlint-cli2 --save-dev
21
21
  ```
22
22
 
23
+ Or [use the container image](#container-image) available on
24
+ [Docker Hub as davidanson/markdownlint-cli2][docker-hub-markdownlint-cli2].
25
+
23
26
  ## Overview
24
27
 
25
28
  - [`markdownlint`][markdownlint] is a library for linting [Markdown][markdown]/
@@ -41,18 +44,23 @@ npm install markdownlint-cli2 --save-dev
41
44
 
42
45
  ## Use
43
46
 
47
+ ### Command Line
48
+
44
49
  ```text
45
50
  markdownlint-cli2 vX.Y.Z (markdownlint vX.Y.Z)
46
51
  https://github.com/DavidAnson/markdownlint-cli2
47
52
 
48
53
  Syntax: markdownlint-cli2 glob0 [glob1] [...] [globN]
54
+ markdownlint-cli2-fix glob0 [glob1] [...] [globN]
55
+ markdownlint-cli2-config config-file glob0 [glob1] [...] [globN]
49
56
 
50
57
  Glob expressions (from the globby library):
51
58
  - * matches any number of characters, but not /
52
59
  - ? matches a single character, but not /
53
- - ** matches any number of characters, including / (when it's the only thing in a path part)
60
+ - ** matches any number of characters, including /
54
61
  - {} allows for a comma-separated list of "or" expressions
55
62
  - ! or # at the beginning of a pattern negate the match
63
+ - : at the beginning identifies a literal file path
56
64
 
57
65
  Dot-only glob:
58
66
  - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended
@@ -62,10 +70,10 @@ Dot-only glob:
62
70
  Configuration via:
63
71
  - .markdownlint-cli2.jsonc
64
72
  - .markdownlint-cli2.yaml
65
- - .markdownlint-cli2.js
73
+ - .markdownlint-cli2.cjs
66
74
  - .markdownlint.jsonc or .markdownlint.json
67
75
  - .markdownlint.yaml or .markdownlint.yml
68
- - .markdownlint.js
76
+ - .markdownlint.cjs
69
77
 
70
78
  Cross-platform compatibility:
71
79
  - UNIX and Windows shells expand globs according to different rules; quoting arguments is recommended
@@ -74,31 +82,74 @@ Cross-platform compatibility:
74
82
  - Some UNIX shells parse exclamation (!) in double-quotes; hashtag (#) is recommended in these cases
75
83
  - The path separator is forward slash (/) on all platforms; backslash (\) is automatically converted
76
84
 
77
- Therefore, the most compatible glob syntax for cross-platform support:
85
+ The most compatible syntax for cross-platform support:
78
86
  $ markdownlint-cli2 "**/*.md" "#node_modules"
79
87
  ```
80
88
 
81
89
  For scenarios where it is preferable to specify glob expressions in a
82
90
  configuration file, the `globs` property of `.markdownlint-cli2.jsonc` or
83
- `.markdownlint-cli2.yaml` or `.markdownlint-cli2.js` may be used instead of (or
91
+ `.markdownlint-cli2.yaml` or `.markdownlint-cli2.cjs` may be used instead of (or
84
92
  in addition to) passing `glob0 ... globN` on the command-line.
85
93
 
86
- As shown above, the default command-line for `markdownlint-cli2` looks something
94
+ As shown above, a typical command-line for `markdownlint-cli2` looks something
87
95
  like:
88
96
 
89
97
  ```bash
90
98
  markdownlint-cli2 "**/*.md" "#node_modules"
91
99
  ```
92
100
 
93
- However, because sharing configuration between "normal" and "fix" modes is so
101
+ Because sharing the same configuration between "normal" and "fix" modes is
94
102
  common, the following command defaults the `fix` property (see below) to `true`:
95
103
 
96
104
  ```bash
97
105
  markdownlint-cli2-fix "**/*.md" "#node_modules"
98
106
  ```
99
107
 
100
- Other than the default behavior of the `fix` property (which can be overridden
101
- in both cases), these two commands behave identically.
108
+ Other than the default behavior of the `fix` property (which can be overridden),
109
+ these two commands behave identically.
110
+
111
+ In cases where it is not convenient to store a configuration file in the root
112
+ of a project, the `markdownlint-cli2-config` command can be used. This command
113
+ accepts a path to any supported configuration file as its first argument:
114
+
115
+ ```bash
116
+ markdownlint-cli2-config "config/.markdownlint-cli2.jsonc" "**/*.md" "#node_modules"
117
+ ```
118
+
119
+ Otherwise, this command behaves identically to `markdownlint-cli2`.
120
+
121
+ ### Container Image
122
+
123
+ A container image [`davidanson/markdownlint-cli2`][docker-hub-markdownlint-cli2]
124
+ can also be used (e.g., as part of a CI pipeline):
125
+
126
+ ```bash
127
+ docker run -v $PWD:/workdir davidanson/markdownlint-cli2:0.4.0 "**/*.md" "#node_modules"
128
+ ```
129
+
130
+ Notes:
131
+
132
+ - As when using the [command line](#command-line), glob patterns are passed as
133
+ arguments.
134
+ - This image is built on the official [Node.js Docker image][nodejs-docker].
135
+ Per security best practices, the [default user `node`][nodejs-docker-non-root]
136
+ runs with restricted permissions. If it is necessary to run as `root`, pass
137
+ the `-u root` option when invoking `docker`.
138
+ - By default, `markdownlint-cli2` will execute within the `/workdir` directory
139
+ *inside the container*. So, as shown above, [bind mount][docker-bind-mounts]
140
+ the project's directory there.
141
+ - A custom working directory can be specified with Docker's `-w` flag:
142
+
143
+ ```bash
144
+ docker run -w /myfolder -v $PWD:/myfolder davidanson/markdownlint-cli2:0.4.0 "**/*.md" "#node_modules"
145
+ ```
146
+
147
+ To invoke the `markdownlint-cli2-config` or `markdownlint-cli2-fix` commands
148
+ instead, use Docker's `--entrypoint` flag:
149
+
150
+ ```bash
151
+ docker run -v $PWD:/workdir --entrypoint="markdownlint-cli2-fix" davidanson/markdownlint-cli2:0.4.0 "**/*.md" "#node_modules"
152
+ ```
102
153
 
103
154
  ### Exit Codes
104
155
 
@@ -207,7 +258,7 @@ in both cases), these two commands behave identically.
207
258
  - For example: [`.markdownlint-cli2.yaml`][markdownlint-cli2-yaml] with all
208
259
  properties set
209
260
 
210
- ### `.markdownlint-cli2.js`
261
+ ### `.markdownlint-cli2.cjs`
211
262
 
212
263
  - The format of this file is a [CommonJS module][commonjs-module] that exports
213
264
  the object described above for `.markdownlint-cli2.jsonc`.
@@ -217,7 +268,7 @@ in both cases), these two commands behave identically.
217
268
  - Other details are the same as for `.markdownlint-cli2.jsonc` described above.
218
269
  - If a `.markdownlint-cli2.jsonc` or `.markdownlint-cli2.yaml` file is present
219
270
  in the same directory, it takes precedence.
220
- - For example: [`.markdownlint-cli2.js`][markdownlint-cli2-js]
271
+ - For example: [`.markdownlint-cli2.cjs`][markdownlint-cli2-cjs]
221
272
 
222
273
  ### `.markdownlint.jsonc` or `.markdownlint.json`
223
274
 
@@ -244,14 +295,14 @@ in both cases), these two commands behave identically.
244
295
  precedence.
245
296
  - For example: [`.markdownlint.yaml`][markdownlint-yaml]
246
297
 
247
- ### `.markdownlint.js`
298
+ ### `.markdownlint.cjs`
248
299
 
249
300
  - The format of this file is a [CommonJS module][commonjs-module] that exports
250
301
  the [`markdownlint` `config` object][markdownlint-config].
251
302
  - Other details are the same as for `jsonc`/`json` files described above.
252
303
  - If a `jsonc`, `json`, `yaml`, or `yml` file is present in the same directory,
253
304
  it takes precedence.
254
- - For example: [`.markdownlint.js`][markdownlint-js]
305
+ - For example: [`.markdownlint.cjs`][markdownlint-cjs]
255
306
 
256
307
  ## Compatibility
257
308
 
@@ -275,7 +326,7 @@ reference to the `repos` list in that project's `.pre-commit-config.yaml` like:
275
326
 
276
327
  ```yaml
277
328
  - repo: https://github.com/DavidAnson/markdownlint-cli2
278
- rev: v0.2.0
329
+ rev: v0.4.0
279
330
  hooks:
280
331
  - id: markdownlint-cli2
281
332
  ```
@@ -304,11 +355,17 @@ reference to the `repos` list in that project's `.pre-commit-config.yaml` like:
304
355
  - 0.1.2 - Update use of `require` to be more flexible
305
356
  - 0.1.3 - Support rule collections
306
357
  - 0.2.0 - Improve handling of Windows paths using backslash
358
+ - 0.3.0 - Add Docker container, update dependencies
359
+ - 0.3.1 - Extensibility tweaks
360
+ - 0.3.2 - Extensibility/Windows/consistency improvements
361
+ - 0.4.0 - New rules, async custom rules, explicit config, CJS (breaking)
307
362
 
308
363
  <!-- markdownlint-disable line-length -->
309
364
 
310
365
  [commonmark]: https://commonmark.org/
311
366
  [commonjs-module]: https://nodejs.org/api/modules.html#modules_modules_commonjs_modules
367
+ [docker-bind-mounts]: https://docs.docker.com/storage/bind-mounts/
368
+ [docker-hub-markdownlint-cli2]: https://hub.docker.com/r/davidanson/markdownlint-cli2
312
369
  [front-matter]: https://jekyllrb.com/docs/frontmatter/
313
370
  [globby]: https://www.npmjs.com/package/globby
314
371
  [html-comment]: https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML/Getting_started
@@ -331,14 +388,16 @@ reference to the `repos` list in that project's `.pre-commit-config.yaml` like:
331
388
  [markdownlint-cli2]: https://github.com/DavidAnson/markdownlint-cli2
332
389
  [markdownlint-cli2-blog]: https://dlaa.me/blog/post/markdownlintcli2
333
390
  [markdownlint-cli2-formatter]: https://www.npmjs.com/search?q=keywords:markdownlint-cli2-formatter
334
- [markdownlint-cli2-js]: test/markdownlint-cli2-js/.markdownlint-cli2.js
391
+ [markdownlint-cli2-cjs]: test/markdownlint-cli2-cjs/.markdownlint-cli2.cjs
335
392
  [markdownlint-cli2-jsonc]: test/markdownlint-cli2-jsonc-example/.markdownlint-cli2.jsonc
336
393
  [markdownlint-cli2-yaml]: test/markdownlint-cli2-yaml-example/.markdownlint-cli2.yaml
337
- [markdownlint-js]: test/markdownlint-js/.markdownlint.js
394
+ [markdownlint-cjs]: test/markdownlint-cjs/.markdownlint.cjs
338
395
  [markdownlint-jsonc]: https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.jsonc
339
396
  [markdownlint-rule]: https://www.npmjs.com/search?q=keywords:markdownlint-rule
340
397
  [markdownlint-yaml]: https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml
341
398
  [nodejs]: https://nodejs.org/
399
+ [nodejs-docker]: https://github.com/nodejs/docker-node
400
+ [nodejs-docker-non-root]: https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md#non-root-user
342
401
  [nodejs-require]: https://nodejs.org/api/modules.html#modules_require_id
343
402
  [npm-image]: https://img.shields.io/npm/v/markdownlint-cli2.svg
344
403
  [npm-url]: https://www.npmjs.com/package/markdownlint-cli2
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+
3
+ // @ts-check
4
+
5
+ "use strict";
6
+
7
+ const { run } = require("./markdownlint-cli2");
8
+
9
+ run({
10
+ "name": "markdownlint-cli2-config"
11
+ });
@@ -7,5 +7,6 @@
7
7
  const { run } = require("./markdownlint-cli2");
8
8
 
9
9
  run({
10
- "fixDefault": true
10
+ "fixDefault": true,
11
+ "name": "markdownlint-cli2-fix"
11
12
  });
@@ -10,9 +10,7 @@ const dynamicRequire = (typeof __non_webpack_require__ === "undefined") ? requir
10
10
  // Capture native require implementation for dynamic loading of modules
11
11
 
12
12
  // Requires
13
- const fs = require("fs").promises;
14
13
  const path = require("path");
15
- const globby = require("globby");
16
14
  const markdownlintLibrary = require("markdownlint");
17
15
  const { markdownlint, "readConfig": markdownlintReadConfig } =
18
16
  markdownlintLibrary.promises;
@@ -23,7 +21,7 @@ const resolveAndRequire = require("./resolve-and-require");
23
21
 
24
22
  // Variables
25
23
  const packageName = "markdownlint-cli2";
26
- const packageVersion = "0.2.0";
24
+ const packageVersion = "0.4.0";
27
25
  const libraryName = "markdownlint";
28
26
  const libraryVersion = markdownlintLibrary.getVersion();
29
27
  const dotOnlySubstitute = "*.{md,markdown}";
@@ -32,10 +30,16 @@ const utf8 = "utf8";
32
30
  // No-op function
33
31
  const noop = () => null;
34
32
 
35
- // Parse JSONC text
36
- const jsoncParse = (text) => JSON.parse(require("strip-json-comments")(text));
33
+ // Gets a synchronous function to parse JSONC text
34
+ const getJsoncParse = async () => {
35
+ const { "default": stripJsonComments } =
36
+ // eslint-disable-next-line max-len
37
+ // eslint-disable-next-line no-inline-comments, node/no-unsupported-features/es-syntax
38
+ await import(/* webpackMode: "eager" */ "strip-json-comments");
39
+ return (text) => JSON.parse(stripJsonComments(text));
40
+ };
37
41
 
38
- // Parse YAML text
42
+ // Synchronous function to parse YAML text
39
43
  const yamlParse = (text) => require("yaml").parse(text);
40
44
 
41
45
  // Negate a glob
@@ -45,12 +49,17 @@ const negateGlob = (glob) => `!${glob}`;
45
49
  const posixPath = (p) => p.split(path.sep).join(path.posix.sep);
46
50
 
47
51
  // Read a JSON(C) or YAML file and return the object
48
- const readConfig = (dir, name, otherwise) => {
52
+ const readConfig = (fs, dir, name, otherwise) => {
49
53
  const file = path.posix.join(dir, name);
50
- return () => fs.access(file).
54
+ return () => fs.promises.access(file).
51
55
  then(
52
- // @ts-ignore
53
- () => markdownlintReadConfig(file, [ jsoncParse, yamlParse ]),
56
+ () => getJsoncParse().then(
57
+ (jsoncParse) => markdownlintReadConfig(
58
+ file,
59
+ [ jsoncParse, yamlParse ],
60
+ fs
61
+ )
62
+ ),
54
63
  otherwise
55
64
  );
56
65
  };
@@ -81,20 +90,71 @@ const requireIdsAndParams = (dir, idsAndParams, noRequire) => {
81
90
  };
82
91
 
83
92
  // Require a JS file and return the exported object
84
- const requireConfig = (dir, name, noRequire) => {
85
- const file = path.posix.join(dir, name);
86
- // eslint-disable-next-line prefer-promise-reject-errors
87
- return () => (noRequire ? Promise.reject() : fs.access(file)).
93
+ const requireConfig = (fs, dir, name, noRequire) => (
94
+ () => (noRequire
95
+ // eslint-disable-next-line prefer-promise-reject-errors
96
+ ? Promise.reject()
97
+ : fs.promises.access(path.posix.join(dir, name))
98
+ ).
88
99
  then(
89
100
  () => requireResolve(dir, `./${name}`),
90
101
  noop
91
- );
102
+ )
103
+ );
104
+
105
+ // Read an options or config file in any format and return the object
106
+ const readOptionsOrConfig = async (configPath, fs, noRequire) => {
107
+ const basename = path.basename(configPath);
108
+ const dirname = path.dirname(configPath);
109
+ let options = null;
110
+ let config = null;
111
+ if (basename.endsWith(".markdownlint-cli2.jsonc")) {
112
+ const jsoncParse = await getJsoncParse();
113
+ options = jsoncParse(await fs.promises.readFile(configPath, utf8));
114
+ } else if (basename.endsWith(".markdownlint-cli2.yaml")) {
115
+ options = yamlParse(await fs.promises.readFile(configPath, utf8));
116
+ } else if (basename.endsWith(".markdownlint-cli2.cjs")) {
117
+ options = await (requireConfig(fs, dirname, basename, noRequire)());
118
+ } else if (
119
+ basename.endsWith(".markdownlint.jsonc") ||
120
+ basename.endsWith(".markdownlint.json") ||
121
+ basename.endsWith(".markdownlint.yaml") ||
122
+ basename.endsWith(".markdownlint.yml")
123
+ ) {
124
+ const jsoncParse = await getJsoncParse();
125
+ config =
126
+ await markdownlintReadConfig(configPath, [ jsoncParse, yamlParse ], fs);
127
+ } else if (basename.endsWith(".markdownlint.cjs")) {
128
+ config = await (requireConfig(fs, dirname, basename, noRequire)());
129
+ }
130
+ return options || { config };
131
+ };
132
+
133
+ // Filter a list of files to ignore by glob
134
+ const removeIgnoredFiles = (dir, files, ignores) => {
135
+ const micromatch = require("micromatch");
136
+ return micromatch(
137
+ files.map((file) => path.posix.relative(dir, file)),
138
+ ignores
139
+ ).map((file) => path.posix.join(dir, file));
92
140
  };
93
141
 
94
- // Process command-line arguments and return glob patterns
142
+ // Process/normalize command-line arguments and return glob patterns
95
143
  const processArgv = (argv) => {
96
- const globPatterns = argv.map(
97
- (glob) => glob.replace(/^#/u, "!").replace(/\\(?![$()*+?[\]^])/gu, "/")
144
+ const globPatterns = (argv || []).map(
145
+ (glob) => {
146
+ if (glob.startsWith(":")) {
147
+ return glob;
148
+ }
149
+ // Escape RegExp special characters recognized by fast-glob
150
+ // https://github.com/mrmlnc/fast-glob#advanced-syntax
151
+ const specialCharacters = /\\(?![$()*+?[\]^])/gu;
152
+ if (glob.startsWith("\\:")) {
153
+ return `\\:${glob.slice(2).replace(specialCharacters, "/")}`;
154
+ }
155
+ return (glob.startsWith("#") ? `!${glob.slice(1)}` : glob).
156
+ replace(specialCharacters, "/");
157
+ }
98
158
  );
99
159
  if ((globPatterns.length === 1) && (globPatterns[0] === ".")) {
100
160
  // Substitute a more reasonable pattern
@@ -105,31 +165,33 @@ const processArgv = (argv) => {
105
165
 
106
166
  // Show help if missing arguments
107
167
  const showHelp = (logMessage) => {
108
- const { name, homepage } = require("./package.json");
109
168
  /* eslint-disable max-len */
110
- logMessage(`${homepage}
169
+ logMessage(`https://github.com/DavidAnson/markdownlint-cli2
111
170
 
112
- Syntax: ${name} glob0 [glob1] [...] [globN]
171
+ Syntax: markdownlint-cli2 glob0 [glob1] [...] [globN]
172
+ markdownlint-cli2-fix glob0 [glob1] [...] [globN]
173
+ markdownlint-cli2-config config-file glob0 [glob1] [...] [globN]
113
174
 
114
175
  Glob expressions (from the globby library):
115
176
  - * matches any number of characters, but not /
116
177
  - ? matches a single character, but not /
117
- - ** matches any number of characters, including / (when it's the only thing in a path part)
178
+ - ** matches any number of characters, including /
118
179
  - {} allows for a comma-separated list of "or" expressions
119
180
  - ! or # at the beginning of a pattern negate the match
181
+ - : at the beginning identifies a literal file path
120
182
 
121
183
  Dot-only glob:
122
- - The command "${name} ." would lint every file in the current directory tree which is probably not intended
123
- - Instead, it is mapped to "${name} ${dotOnlySubstitute}" which lints all Markdown files in the current directory
124
- - To lint every file in the current directory tree, the command "${name} **" can be used instead
184
+ - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended
185
+ - Instead, it is mapped to "markdownlint-cli2 ${dotOnlySubstitute}" which lints all Markdown files in the current directory
186
+ - To lint every file in the current directory tree, the command "markdownlint-cli2 **" can be used instead
125
187
 
126
188
  Configuration via:
127
189
  - .markdownlint-cli2.jsonc
128
190
  - .markdownlint-cli2.yaml
129
- - .markdownlint-cli2.js
191
+ - .markdownlint-cli2.cjs
130
192
  - .markdownlint.jsonc or .markdownlint.json
131
193
  - .markdownlint.yaml or .markdownlint.yml
132
- - .markdownlint.js
194
+ - .markdownlint.cjs
133
195
 
134
196
  Cross-platform compatibility:
135
197
  - UNIX and Windows shells expand globs according to different rules; quoting arguments is recommended
@@ -138,14 +200,15 @@ Cross-platform compatibility:
138
200
  - Some UNIX shells parse exclamation (!) in double-quotes; hashtag (#) is recommended in these cases
139
201
  - The path separator is forward slash (/) on all platforms; backslash (\\) is automatically converted
140
202
 
141
- Therefore, the most compatible glob syntax for cross-platform support:
142
- $ ${name} "**/*.md" "#node_modules"`
203
+ The most compatible syntax for cross-platform support:
204
+ $ markdownlint-cli2 "**/*.md" "#node_modules"`
143
205
  );
144
206
  /* eslint-enable max-len */
145
207
  };
146
208
 
147
209
  // Get (creating if necessary) and process a directory's info object
148
- const getAndProcessDirInfo = (tasks, dirToDirInfo, dir, noRequire, func) => {
210
+ const getAndProcessDirInfo =
211
+ (fs, tasks, dirToDirInfo, dir, noRequire, func) => {
149
212
  let dirInfo = dirToDirInfo[dir];
150
213
  if (!dirInfo) {
151
214
  dirInfo = {
@@ -163,15 +226,23 @@ const getAndProcessDirInfo = (tasks, dirToDirInfo, dir, noRequire, func) => {
163
226
  const markdownlintCli2Yaml =
164
227
  path.posix.join(dir, ".markdownlint-cli2.yaml");
165
228
  tasks.push(
166
- fs.access(markdownlintCli2Jsonc).
229
+ fs.promises.access(markdownlintCli2Jsonc).
167
230
  then(
168
- () => fs.readFile(markdownlintCli2Jsonc, utf8).then(jsoncParse),
169
- () => fs.access(markdownlintCli2Yaml).
231
+ () => fs.promises.
232
+ readFile(markdownlintCli2Jsonc, utf8).
233
+ then(
234
+ (content) => getJsoncParse().
235
+ then((jsoncParse) => jsoncParse(content))
236
+ ),
237
+ () => fs.promises.access(markdownlintCli2Yaml).
170
238
  then(
171
- () => fs.readFile(markdownlintCli2Yaml, utf8).then(yamlParse),
239
+ () => fs.promises.
240
+ readFile(markdownlintCli2Yaml, utf8).
241
+ then(yamlParse),
172
242
  requireConfig(
243
+ fs,
173
244
  dir,
174
- ".markdownlint-cli2.js",
245
+ ".markdownlint-cli2.cjs",
175
246
  noRequire
176
247
  )
177
248
  )
@@ -184,20 +255,25 @@ const getAndProcessDirInfo = (tasks, dirToDirInfo, dir, noRequire, func) => {
184
255
  // Load markdownlint object(s)
185
256
  const readConfigs =
186
257
  readConfig(
258
+ fs,
187
259
  dir,
188
260
  ".markdownlint.jsonc",
189
261
  readConfig(
262
+ fs,
190
263
  dir,
191
264
  ".markdownlint.json",
192
265
  readConfig(
266
+ fs,
193
267
  dir,
194
268
  ".markdownlint.yaml",
195
269
  readConfig(
270
+ fs,
196
271
  dir,
197
272
  ".markdownlint.yml",
198
273
  requireConfig(
274
+ fs,
199
275
  dir,
200
- ".markdownlint.js",
276
+ ".markdownlint.cjs",
201
277
  noRequire
202
278
  )
203
279
  )
@@ -218,11 +294,18 @@ const getAndProcessDirInfo = (tasks, dirToDirInfo, dir, noRequire, func) => {
218
294
  };
219
295
 
220
296
  // Get base markdownlint-cli2 options object
221
- const getBaseOptions =
222
- async (baseDir, globPatterns, optionsDefault, fixDefault, noRequire) => {
297
+ const getBaseOptions = async (
298
+ fs,
299
+ baseDir,
300
+ globPatterns,
301
+ optionsDefault,
302
+ fixDefault,
303
+ noGlobs,
304
+ noRequire
305
+ ) => {
223
306
  const tasks = [];
224
307
  const dirToDirInfo = {};
225
- getAndProcessDirInfo(tasks, dirToDirInfo, baseDir, noRequire);
308
+ getAndProcessDirInfo(fs, tasks, dirToDirInfo, baseDir, noRequire);
226
309
  await Promise.all(tasks);
227
310
  // eslint-disable-next-line no-multi-assign
228
311
  const baseMarkdownlintOptions = dirToDirInfo[baseDir].markdownlintOptions =
@@ -231,16 +314,17 @@ async (baseDir, globPatterns, optionsDefault, fixDefault, noRequire) => {
231
314
  dirToDirInfo[baseDir].markdownlintOptions
232
315
  );
233
316
 
234
- // Append any globs specified in markdownlint-cli2 configuration
235
- const globs = baseMarkdownlintOptions.globs || [];
236
- appendToArray(globPatterns, globs);
317
+ if (!noGlobs) {
318
+ // Append any globs specified in markdownlint-cli2 configuration
319
+ const globs = baseMarkdownlintOptions.globs || [];
320
+ appendToArray(globPatterns, globs);
321
+ }
237
322
 
238
323
  // Pass base ignore globs as globby patterns (best performance)
239
324
  const ignorePatterns =
240
325
  // eslint-disable-next-line unicorn/no-array-callback-reference
241
326
  (baseMarkdownlintOptions.ignores || []).map(negateGlob);
242
327
  appendToArray(globPatterns, ignorePatterns);
243
- delete baseMarkdownlintOptions.ignores;
244
328
 
245
329
  return {
246
330
  baseMarkdownlintOptions,
@@ -250,24 +334,83 @@ async (baseDir, globPatterns, optionsDefault, fixDefault, noRequire) => {
250
334
 
251
335
  // Enumerate files from globs and build directory infos
252
336
  const enumerateFiles =
253
- async (baseDir, globPatterns, dirToDirInfo, noRequire) => {
337
+ // eslint-disable-next-line max-len
338
+ async (fs, baseDirSystem, baseDir, globPatterns, dirToDirInfo, noErrors, noRequire) => {
254
339
  const tasks = [];
255
340
  const globbyOptions = {
256
341
  "absolute": true,
257
- "cwd": baseDir
342
+ "cwd": baseDir,
343
+ "expandDirectories": false,
344
+ fs
258
345
  };
259
- for await (const file of globby.stream(globPatterns, globbyOptions)) {
260
- // @ts-ignore
346
+ if (noErrors) {
347
+ globbyOptions.suppressErrors = true;
348
+ }
349
+ // Special-case literal files
350
+ const literalFiles = [];
351
+ const filteredGlobPatterns = globPatterns.filter(
352
+ (globPattern) => {
353
+ if (globPattern.startsWith(":")) {
354
+ literalFiles.push(
355
+ posixPath(path.resolve(baseDirSystem, globPattern.slice(1)))
356
+ );
357
+ return false;
358
+ }
359
+ return true;
360
+ }
361
+ ).map((globPattern) => globPattern.replace(/^\\:/u, ":"));
362
+ const baseMarkdownlintOptions = dirToDirInfo[baseDir].markdownlintOptions;
363
+ const globsForIgnore =
364
+ (baseMarkdownlintOptions.globs || []).
365
+ filter((glob) => glob.startsWith("!"));
366
+ const filteredLiteralFiles =
367
+ ((literalFiles.length > 0) && (globsForIgnore.length > 0))
368
+ ? removeIgnoredFiles(baseDir, literalFiles, globsForIgnore)
369
+ : literalFiles;
370
+ // Manually expand directories to avoid globby call to dir-glob.sync
371
+ const expandedDirectories = await Promise.all(
372
+ filteredGlobPatterns.map((globPattern) => {
373
+ const barePattern =
374
+ globPattern.startsWith("!")
375
+ ? globPattern.slice(1)
376
+ : globPattern;
377
+ const globPath =
378
+ (path.posix.isAbsolute(barePattern) || path.isAbsolute(barePattern))
379
+ ? barePattern
380
+ : path.posix.join(baseDir, barePattern);
381
+ return fs.promises.stat(globPath).
382
+ then((stats) => (stats.isDirectory()
383
+ ? path.posix.join(globPattern, "**")
384
+ : globPattern)).
385
+ catch(() => globPattern);
386
+ })
387
+ );
388
+ // Process glob patterns
389
+ // eslint-disable-next-line max-len
390
+ // eslint-disable-next-line no-inline-comments, node/no-unsupported-features/es-syntax
391
+ const { globby } = await import(/* webpackMode: "eager" */ "globby");
392
+ const files = [
393
+ ...await globby(expandedDirectories, globbyOptions),
394
+ ...filteredLiteralFiles
395
+ ];
396
+ for (const file of files) {
261
397
  const dir = path.posix.dirname(file);
262
- getAndProcessDirInfo(tasks, dirToDirInfo, dir, noRequire, (dirInfo) => {
263
- dirInfo.files.push(file);
264
- });
398
+ getAndProcessDirInfo(
399
+ fs,
400
+ tasks,
401
+ dirToDirInfo,
402
+ dir,
403
+ noRequire,
404
+ (dirInfo) => {
405
+ dirInfo.files.push(file);
406
+ }
407
+ );
265
408
  }
266
409
  await Promise.all(tasks);
267
410
  };
268
411
 
269
412
  // Enumerate (possibly missing) parent directories and update directory infos
270
- const enumerateParents = async (baseDir, dirToDirInfo, noRequire) => {
413
+ const enumerateParents = async (fs, baseDir, dirToDirInfo, noRequire) => {
271
414
  const tasks = [];
272
415
 
273
416
  // Create a lookup of baseDir and parents
@@ -289,10 +432,17 @@ const enumerateParents = async (baseDir, dirToDirInfo, noRequire) => {
289
432
  ) {
290
433
  lastDir = dir;
291
434
  lastDirInfo =
292
- // eslint-disable-next-line no-loop-func
293
- getAndProcessDirInfo(tasks, dirToDirInfo, dir, noRequire, (dirInfo) => {
294
- lastDirInfo.parent = dirInfo;
295
- });
435
+ getAndProcessDirInfo(
436
+ fs,
437
+ tasks,
438
+ dirToDirInfo,
439
+ dir,
440
+ noRequire,
441
+ // eslint-disable-next-line no-loop-func
442
+ (dirInfo) => {
443
+ lastDirInfo.parent = dirInfo;
444
+ }
445
+ );
296
446
  }
297
447
 
298
448
  // If dir not under baseDir, inject it as parent for configuration
@@ -305,9 +455,23 @@ const enumerateParents = async (baseDir, dirToDirInfo, noRequire) => {
305
455
 
306
456
  // Create directory info objects by enumerating file globs
307
457
  const createDirInfos =
308
- async (baseDir, globPatterns, dirToDirInfo, optionsOverride, noRequire) => {
309
- await enumerateFiles(baseDir, globPatterns, dirToDirInfo, noRequire);
310
- await enumerateParents(baseDir, dirToDirInfo, noRequire);
458
+ // eslint-disable-next-line max-len
459
+ async (fs, baseDirSystem, baseDir, globPatterns, dirToDirInfo, optionsOverride, noErrors, noRequire) => {
460
+ await enumerateFiles(
461
+ fs,
462
+ baseDirSystem,
463
+ baseDir,
464
+ globPatterns,
465
+ dirToDirInfo,
466
+ noErrors,
467
+ noRequire
468
+ );
469
+ await enumerateParents(
470
+ fs,
471
+ baseDir,
472
+ dirToDirInfo,
473
+ noRequire
474
+ );
311
475
 
312
476
  // Merge file lists with identical configuration
313
477
  const dirs = Object.keys(dirToDirInfo);
@@ -414,34 +578,34 @@ async (baseDir, globPatterns, dirToDirInfo, optionsOverride, noRequire) => {
414
578
  };
415
579
 
416
580
  // Lint files in groups by shared configuration
417
- const lintFiles = (dirInfos, fileContents) => {
581
+ const lintFiles = (fs, dirInfos, fileContents) => {
418
582
  const tasks = [];
419
583
  // For each dirInfo
420
584
  for (const dirInfo of dirInfos) {
421
585
  const { dir, files, markdownlintConfig, markdownlintOptions } = dirInfo;
422
586
  // Filter file/string inputs to only those in the dirInfo
423
- const filteredFileContents = {};
424
- for (const file in fileContents) {
425
- if (files.includes(file)) {
426
- filteredFileContents[file] = fileContents[file];
427
- }
587
+ let filesAfterIgnores = files;
588
+ if (
589
+ markdownlintOptions.ignores &&
590
+ (markdownlintOptions.ignores.length > 0)
591
+ ) {
592
+ // eslint-disable-next-line unicorn/no-array-callback-reference
593
+ const ignores = markdownlintOptions.ignores.map(negateGlob);
594
+ filesAfterIgnores = removeIgnoredFiles(dir, files, ignores);
428
595
  }
429
- let filteredFiles = files.filter(
596
+ const filteredFiles = filesAfterIgnores.filter(
430
597
  (file) => fileContents[file] === undefined
431
598
  );
432
- if (markdownlintOptions.ignores) {
433
- // eslint-disable-next-line unicorn/no-array-callback-reference
434
- const ignores = markdownlintOptions.ignores.map(negateGlob);
435
- const micromatch = require("micromatch");
436
- filteredFiles = micromatch(
437
- files.map((file) => path.posix.relative(dir, file)),
438
- ignores
439
- ).map((file) => path.posix.join(dir, file));
599
+ const filteredStrings = {};
600
+ for (const file of filesAfterIgnores) {
601
+ if (fileContents[file] !== undefined) {
602
+ filteredStrings[file] = fileContents[file];
603
+ }
440
604
  }
441
605
  // Create markdownlint options object
442
606
  const options = {
443
607
  "files": filteredFiles,
444
- "strings": filteredFileContents,
608
+ "strings": filteredStrings,
445
609
  "config": markdownlintConfig || markdownlintOptions.config,
446
610
  "customRules": markdownlintOptions.customRules,
447
611
  "frontMatter": markdownlintOptions.frontMatter
@@ -450,7 +614,8 @@ const lintFiles = (dirInfos, fileContents) => {
450
614
  "handleRuleFailures": true,
451
615
  "markdownItPlugins": markdownlintOptions.markdownItPlugins,
452
616
  "noInlineConfig": Boolean(markdownlintOptions.noInlineConfig),
453
- "resultVersion": 3
617
+ "resultVersion": 3,
618
+ fs
454
619
  };
455
620
  // Invoke markdownlint
456
621
  // @ts-ignore
@@ -467,11 +632,11 @@ const lintFiles = (dirInfos, fileContents) => {
467
632
  if (errorInfos.length > 0) {
468
633
  delete results[fileName];
469
634
  options.files.push(fileName);
470
- subTasks.push(fs.readFile(fileName, utf8).
635
+ subTasks.push(fs.promises.readFile(fileName, utf8).
471
636
  then((original) => {
472
637
  const fixed = markdownlintRuleHelpers.
473
638
  applyFixes(original, errorInfos);
474
- return fs.writeFile(fileName, fixed, utf8);
639
+ return fs.promises.writeFile(fileName, fixed, utf8);
475
640
  })
476
641
  );
477
642
  }
@@ -555,26 +720,40 @@ const main = async (params) => {
555
720
  fixDefault,
556
721
  fileContents,
557
722
  nonFileContents,
558
- noRequire
723
+ noErrors,
724
+ noGlobs,
725
+ noRequire,
726
+ name
559
727
  } = params;
560
728
  const logMessage = params.logMessage || noop;
561
729
  const logError = params.logError || noop;
730
+ const fs = params.fs || require("fs");
562
731
  const baseDirSystem =
563
732
  (directory && path.resolve(directory)) ||
564
733
  process.cwd();
565
734
  const baseDir = posixPath(baseDirSystem);
566
735
  // Output banner
567
736
  logMessage(
568
- `${packageName} v${packageVersion} (${libraryName} v${libraryVersion})`
737
+ // eslint-disable-next-line max-len
738
+ `${name || packageName} v${packageVersion} (${libraryName} v${libraryVersion})`
569
739
  );
740
+ // Read argv configuration file (if relevant and present)
741
+ let optionsArgv = null;
742
+ const [ configPath ] = (argv || []);
743
+ if ((name === "markdownlint-cli2-config") && configPath) {
744
+ optionsArgv =
745
+ await readOptionsOrConfig(configPath, fs, noRequire);
746
+ }
570
747
  // Process arguments and get base options
571
- const globPatterns = processArgv(argv);
748
+ const globPatterns = processArgv(optionsArgv ? argv.slice(1) : argv);
572
749
  const { baseMarkdownlintOptions, dirToDirInfo } =
573
750
  await getBaseOptions(
751
+ fs,
574
752
  baseDir,
575
753
  globPatterns,
576
- optionsDefault,
754
+ optionsArgv || optionsDefault,
577
755
  fixDefault,
756
+ noGlobs,
578
757
  noRequire
579
758
  );
580
759
  if ((globPatterns.length === 0) && !nonFileContents) {
@@ -603,10 +782,13 @@ const main = async (params) => {
603
782
  // Create linting tasks
604
783
  const dirInfos =
605
784
  await createDirInfos(
785
+ fs,
786
+ baseDirSystem,
606
787
  baseDir,
607
788
  globPatterns,
608
789
  dirToDirInfo,
609
790
  optionsOverride,
791
+ noErrors,
610
792
  noRequire
611
793
  );
612
794
  // Output linting status
@@ -618,7 +800,7 @@ const main = async (params) => {
618
800
  logMessage(`Linting: ${fileCount} file(s)`);
619
801
  }
620
802
  // Lint files
621
- const lintResults = await lintFiles(dirInfos, resolvedFileContents);
803
+ const lintResults = await lintFiles(fs, dirInfos, resolvedFileContents);
622
804
  // Output summary
623
805
  const summary = createSummary(baseDir, lintResults);
624
806
  if (showProgress) {
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "markdownlint-cli2",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "A fast, flexible, configuration-based command-line interface for linting Markdown/CommonMark files with the `markdownlint` library",
5
5
  "author": {
6
6
  "name": "David Anson",
7
7
  "url": "https://dlaa.me/"
8
8
  },
9
9
  "license": "MIT",
10
+ "type": "commonjs",
10
11
  "main": "markdownlint-cli2.js",
11
12
  "bin": {
12
13
  "markdownlint-cli2": "markdownlint-cli2.js",
14
+ "markdownlint-cli2-config": "markdownlint-cli2-config.js",
13
15
  "markdownlint-cli2-fix": "markdownlint-cli2-fix.js"
14
16
  },
15
17
  "homepage": "https://github.com/DavidAnson/markdownlint-cli2",
@@ -19,12 +21,21 @@
19
21
  },
20
22
  "bugs": "https://github.com/DavidAnson/markdownlint-cli2/issues",
21
23
  "scripts": {
24
+ "build-docker-image": "VERSION=$(node -e \"process.stdout.write(require('./package.json').version)\") && docker build -t davidanson/markdownlint-cli2:$VERSION -f docker/Dockerfile .",
22
25
  "ci": "npm-run-all --continue-on-error --parallel test-cover lint",
26
+ "docker-npm-install": "docker run --rm --tty --name npm-install --volume $PWD:/home/workdir --workdir /home/workdir --user node node:16 npm install",
27
+ "docker-npm-run-upgrade": "docker run --rm --tty --name npm-run-upgrade --volume $PWD:/home/workdir --workdir /home/workdir --user node node:16 npm run upgrade",
23
28
  "lint": "eslint --max-warnings 0 .",
29
+ "lint-dockerfile": "docker run --rm -i hadolint/hadolint:latest-alpine < docker/Dockerfile",
24
30
  "lint-watch": "git ls-files | entr npm run lint",
25
- "test": "ava test/append-to-array-test.js test/markdownlint-cli2-test.js test/markdownlint-cli2-test-exec.js test/markdownlint-cli2-test-main.js test/merge-options-test.js test/resolve-and-require-test.js",
31
+ "publish-docker-image": "VERSION=$(node -e \"process.stdout.write(require('./package.json').version)\") && docker buildx build --platform linux/arm64,linux/amd64 -t davidanson/markdownlint-cli2:$VERSION -t davidanson/markdownlint-cli2:latest -f docker/Dockerfile --push .",
32
+ "test": "ava --timeout=1m test/append-to-array-test.js test/fs-mock-test.js test/markdownlint-cli2-test.js test/markdownlint-cli2-test-exec.js test/markdownlint-cli2-test-fs.js test/markdownlint-cli2-test-main.js test/merge-options-test.js test/resolve-and-require-test.js",
33
+ "test-docker-image": "VERSION=$(node -e \"process.stdout.write(require('./package.json').version)\") && docker run --rm -v $PWD:/workdir davidanson/markdownlint-cli2:$VERSION \"*.md\"",
34
+ "test-docker-hub-image": "VERSION=$(node -e \"process.stdout.write(require('./package.json').version)\") && docker image rm davidanson/markdownlint-cli2:$VERSION davidanson/markdownlint-cli2:latest || true && docker run --rm -v $PWD:/workdir davidanson/markdownlint-cli2:$VERSION \"*.md\" && docker run --rm -v $PWD:/workdir davidanson/markdownlint-cli2:latest \"*.md\"",
26
35
  "test-cover": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 npm test",
27
- "test-watch": "git ls-files | entr npm run test"
36
+ "test-watch": "git ls-files | entr npm run test",
37
+ "update-snapshots": "ava --update-snapshots test/markdownlint-cli2-test-exec.js test/markdownlint-cli2-test-fs.js test/markdownlint-cli2-test-main.js",
38
+ "upgrade": "npx --yes npm-check-updates --upgrade"
28
39
  },
29
40
  "engines": {
30
41
  "node": ">=12"
@@ -32,37 +43,39 @@
32
43
  "files": [
33
44
  "append-to-array.js",
34
45
  "markdownlint-cli2.js",
46
+ "markdownlint-cli2-config.js",
35
47
  "markdownlint-cli2-fix.js",
36
48
  "merge-options.js",
37
49
  "resolve-and-require.js"
38
50
  ],
39
51
  "dependencies": {
40
- "globby": "~11.0.4",
41
- "markdownlint": "~0.23.1",
42
- "markdownlint-cli2-formatter-default": "^0.0.2",
43
- "markdownlint-rule-helpers": "~0.14.0",
44
- "micromatch": "~4.0.4",
45
- "strip-json-comments": "~3.1.1",
46
- "yaml": "~1.10.2"
52
+ "globby": "12.1.0",
53
+ "markdownlint": "0.25.1",
54
+ "markdownlint-cli2-formatter-default": "0.0.3",
55
+ "markdownlint-rule-helpers": "0.16.0",
56
+ "micromatch": "4.0.4",
57
+ "strip-json-comments": "4.0.0",
58
+ "yaml": "1.10.2"
47
59
  },
48
60
  "devDependencies": {
49
- "@iktakahiro/markdown-it-katex": "~4.0.1",
50
- "ava": "~3.15.0",
51
- "c8": "~7.7.3",
52
- "cpy": "~8.1.2",
53
- "del": "~6.0.0",
54
- "eslint": "~7.31.0",
55
- "eslint-plugin-node": "~11.1.0",
56
- "eslint-plugin-unicorn": "~34.0.1",
57
- "execa": "~5.1.1",
58
- "markdown-it-emoji": "~2.0.0",
59
- "markdown-it-for-inline": "~0.1.1",
60
- "markdownlint-cli2-formatter-json": "^0.0.4",
61
- "markdownlint-cli2-formatter-junit": "^0.0.3",
62
- "markdownlint-cli2-formatter-pretty": "^0.0.2",
63
- "markdownlint-cli2-formatter-summarize": "^0.0.3",
64
- "markdownlint-rule-titlecase": "~0.1.0",
65
- "npm-run-all": "~4.1.5"
61
+ "@iktakahiro/markdown-it-katex": "4.0.1",
62
+ "ava": "4.0.1",
63
+ "c8": "7.11.0",
64
+ "cpy": "8.1.2",
65
+ "del": "6.0.0",
66
+ "eslint": "8.6.0",
67
+ "eslint-plugin-node": "11.1.0",
68
+ "eslint-plugin-unicorn": "40.0.0",
69
+ "execa": "6.0.0",
70
+ "markdown-it-emoji": "2.0.0",
71
+ "markdown-it-for-inline": "0.1.1",
72
+ "markdownlint-cli2-formatter-json": "0.0.5",
73
+ "markdownlint-cli2-formatter-junit": "0.0.4",
74
+ "markdownlint-cli2-formatter-pretty": "0.0.3",
75
+ "markdownlint-cli2-formatter-summarize": "0.0.5",
76
+ "markdownlint-rule-github-internal-links": "0.1.0",
77
+ "markdownlint-rule-titlecase": "0.1.0",
78
+ "npm-run-all": "4.1.5"
66
79
  },
67
80
  "keywords": [
68
81
  "markdown",
@@ -4,10 +4,9 @@
4
4
 
5
5
  /**
6
6
  * Wrapper for calling Node's require.resolve/require with an additional path.
7
- *
8
- * @param {Object} req Node's require function (or equivalent).
9
- * @param {*} id Package identifier to require.
10
- * @param {*} dir Directory to include when resolving paths.
7
+ * @param {Object} req Node's require implementation (or equivalent).
8
+ * @param {String} id Package identifier to require.
9
+ * @param {String} dir Directory to include when resolving paths.
11
10
  * @returns {Object} Exported module content.
12
11
  */
13
12
  const resolveAndRequire = (req, id, dir) => {