markdownlint-cli2 0.20.0 → 0.22.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.
@@ -7,22 +7,22 @@ import pathDefault from "node:path";
7
7
  const pathPosix = pathDefault.posix;
8
8
  import { pathToFileURL } from "node:url";
9
9
  import { globby } from "globby";
10
+ import jsonpointer from "jsonpointer";
10
11
  import micromatch from "micromatch";
11
12
  import { applyFixes, getVersion, resolveModule } from "markdownlint";
12
13
  import { lint, extendConfig, readConfig } from "markdownlint/promise";
13
14
  import { expandTildePath } from "markdownlint/helpers";
14
15
  import appendToArray from "./append-to-array.mjs";
16
+ import { cli2SchemaKeys, libraryName, packageName, packageVersion } from "./constants.mjs";
15
17
  import mergeOptions from "./merge-options.mjs";
16
- import parsers from "./parsers/parsers.mjs";
18
+ import allParsers from "./parsers/parsers.mjs";
17
19
  import jsoncParse from "./parsers/jsonc-parse.mjs";
20
+ import tomlParse from "./parsers/toml-parse.mjs";
18
21
  import yamlParse from "./parsers/yaml-parse.mjs";
19
22
 
20
23
  /* eslint-disable jsdoc/reject-any-type */
21
24
 
22
25
  // Variables
23
- const packageName = "markdownlint-cli2";
24
- const packageVersion = "0.20.0";
25
- const libraryName = "markdownlint";
26
26
  const libraryVersion = getVersion();
27
27
  const bannerMessage = `${packageName} v${packageVersion} (${libraryName} v${libraryVersion})`;
28
28
  const dotOnlySubstitute = "*.{md,markdown}";
@@ -34,6 +34,15 @@ const noop = () => null;
34
34
  // Negates a glob
35
35
  const negateGlob = (/** @type {string} */ glob) => `!${glob}`;
36
36
 
37
+ // Reads and parses a JSONC file
38
+ const readJsonc = (/** @type {string} */ file, /** @type {FsLike} */ fs) => fs.promises.readFile(file, utf8).then(jsoncParse);
39
+
40
+ // Reads and parses a TOML file
41
+ const readToml = (/** @type {string} */ file, /** @type {FsLike} */ fs) => fs.promises.readFile(file, utf8).then(tomlParse);
42
+
43
+ // Reads and parses a YAML file
44
+ const readYaml = (/** @type {string} */ file, /** @type {FsLike} */ fs) => fs.promises.readFile(file, utf8).then(yamlParse);
45
+
37
46
  // Throws a meaningful exception for an unusable configuration file
38
47
  const throwForConfigurationFile = (/** @type {string} */ file, /** @type {Error | any} */ error) => {
39
48
  throw new Error(
@@ -50,20 +59,6 @@ const resolveModulePaths = (/** @type {string} */ dir, /** @type {string[]} */ m
50
59
  modulePaths.map((path) => pathDefault.resolve(dir, expandTildePath(path, os)))
51
60
  );
52
61
 
53
- // Read a JSON(C) or YAML file and return the object
54
- const readConfigFile = (/** @type {FsLike} */ fs, /** @type {string} */ dir, /** @type {string} */ name, /** @type {() => void} */ otherwise) => () => {
55
- const file = pathPosix.join(dir, name);
56
- return fs.promises.access(file).
57
- then(
58
- () => readConfig(
59
- file,
60
- parsers,
61
- fs
62
- ),
63
- otherwise
64
- );
65
- };
66
-
67
62
  // Import a module ID with a custom directory in the path
68
63
  const importModule = async (/** @type {string[] | string} */ dirOrDirs, /** @type {string} */ id, /** @type {boolean} */ noImport) => {
69
64
  if (typeof id !== "string") {
@@ -118,19 +113,10 @@ const importModuleIdsAndParams = (/** @type {string[]} */ dirs, /** @type {strin
118
113
  ).then((results) => results.filter(Boolean))
119
114
  );
120
115
 
121
- // Import a JavaScript file and return the exported object
122
- const importConfig = (/** @type {FsLike} */ fs, /** @type {string} */ dir, /** @type {string} */ name, /** @type {boolean} */ noImport, /** @type {() => void} */ otherwise) => () => {
123
- const file = pathPosix.join(dir, name);
124
- return fs.promises.access(file).
125
- then(
126
- () => importModule(dir, name, noImport),
127
- otherwise
128
- );
129
- };
130
-
131
116
  // Extend a config object if it has 'extends' property
132
- const getExtendedConfig = (/** @type {import("markdownlint").Configuration} */ config, /** @type {string} */ configPath, /** @type {FsLike} */ fs) => {
117
+ const getExtendedConfig = (/** @type {ExecutionContext} */ context, /** @type {Configuration} */ config, /** @type {string} */ configPath) => {
133
118
  if (config.extends) {
119
+ const { fs, parsers } = context;
134
120
  return extendConfig(
135
121
  config,
136
122
  configPath,
@@ -143,50 +129,64 @@ const getExtendedConfig = (/** @type {import("markdownlint").Configuration} */ c
143
129
  };
144
130
 
145
131
  // Read an options or config file in any format and return the object
146
- const readOptionsOrConfig = async (/** @type {string} */ configPath, /** @type {FsLike} */ fs, /** @type {boolean} */ noImport) => {
132
+ const readOptionsOrConfig = async (/** @type {ExecutionContext} */ context, /** @type {string} */ configPath, /** @type {string | undefined} */ configPointer) => {
133
+ const { fs, noImport } = context;
147
134
  const basename = pathPosix.basename(configPath);
148
135
  const dirname = pathPosix.dirname(configPath);
149
136
  let options = null;
150
137
  let config = null;
138
+ let unknown = null;
151
139
  try {
152
140
  if (basename.endsWith(".markdownlint-cli2.jsonc")) {
153
- options = jsoncParse(await fs.promises.readFile(configPath, utf8));
141
+ options = await readJsonc(configPath, fs);
154
142
  } else if (basename.endsWith(".markdownlint-cli2.yaml")) {
155
- options = yamlParse(await fs.promises.readFile(configPath, utf8));
156
- } else if (
157
- basename.endsWith(".markdownlint-cli2.cjs") ||
158
- basename.endsWith(".markdownlint-cli2.mjs")
159
- ) {
143
+ options = await readYaml(configPath, fs);
144
+ } else if (basename.endsWith(".markdownlint-cli2.cjs") || basename.endsWith(".markdownlint-cli2.mjs")) {
160
145
  options = await importModule(dirname, basename, noImport);
161
- } else if (
162
- basename.endsWith(".markdownlint.jsonc") ||
163
- basename.endsWith(".markdownlint.json") ||
164
- basename.endsWith(".markdownlint.yaml") ||
165
- basename.endsWith(".markdownlint.yml")
166
- ) {
167
- config = await readConfig(configPath, parsers, fs);
168
- } else if (
169
- basename.endsWith(".markdownlint.cjs") ||
170
- basename.endsWith(".markdownlint.mjs")
171
- ) {
146
+ } else if (basename.endsWith(".markdownlint.jsonc") || basename.endsWith(".markdownlint.json")) {
147
+ config = await readJsonc(configPath, fs);
148
+ } else if (basename.endsWith(".markdownlint.yaml") || basename.endsWith(".markdownlint.yml")) {
149
+ config = await readYaml(configPath, fs);
150
+ } else if (basename.endsWith(".markdownlint.cjs") || basename.endsWith(".markdownlint.mjs")) {
172
151
  config = await importModule(dirname, basename, noImport);
152
+ } else if (basename.endsWith(".jsonc") || basename.endsWith(".json")) {
153
+ unknown = await readJsonc(configPath, fs);
154
+ } else if (basename.endsWith(".toml")) {
155
+ unknown = await readToml(configPath, fs);
156
+ } else if (basename.endsWith(".yaml") || basename.endsWith(".yml")) {
157
+ unknown = await readYaml(configPath, fs);
158
+ } else if (basename.endsWith(".cjs") || basename.endsWith(".mjs")) {
159
+ unknown = await importModule(dirname, basename, noImport);
173
160
  } else {
174
161
  throw new Error(
175
162
  "Configuration file should be one of the supported names " +
176
163
  "(e.g., '.markdownlint-cli2.jsonc') or a prefix with a supported name " +
177
- "(e.g., 'example.markdownlint-cli2.jsonc')."
164
+ "(e.g., 'example.markdownlint-cli2.jsonc') or have a supported extension " +
165
+ "(e.g., jsonc, json, yaml, yml, cjs, mjs)."
178
166
  );
179
167
  }
180
168
  } catch (error) {
181
169
  throwForConfigurationFile(configPath, error);
182
170
  }
171
+ if (configPointer) {
172
+ const objects = [ options, config, unknown ];
173
+ [ options, config, unknown ] = objects.map((obj) => obj && (jsonpointer.get(obj, configPointer) || {}));
174
+ }
175
+ if (unknown) {
176
+ const keys = Object.keys(unknown);
177
+ if (keys.some((key) => cli2SchemaKeys.has(key))) {
178
+ options = unknown;
179
+ } else {
180
+ config = unknown;
181
+ }
182
+ }
183
183
  if (options) {
184
184
  if (options.config) {
185
- options.config = await getExtendedConfig(options.config, configPath, fs);
185
+ options.config = await getExtendedConfig(context, options.config, configPath);
186
186
  }
187
187
  return options;
188
188
  }
189
- config = await getExtendedConfig(config, configPath, fs);
189
+ config = await getExtendedConfig(context, config, configPath);
190
190
  return { config };
191
191
  };
192
192
 
@@ -229,7 +229,7 @@ const showHelp = (/** @type {Logger} */ logMessage, /** @type {boolean} */ showB
229
229
  }
230
230
  logMessage(`https://github.com/DavidAnson/markdownlint-cli2
231
231
 
232
- Syntax: markdownlint-cli2 glob0 [glob1] [...] [globN] [--config file] [--fix] [--format] [--help] [--no-globs]
232
+ Syntax: markdownlint-cli2 glob0 [glob1] [...] [globN] [--config file] [--configPointer pointer] [--fix] [--format] [--help] [--no-globs]
233
233
 
234
234
  Glob expressions (from the globby library):
235
235
  - * matches any number of characters, but not /
@@ -246,11 +246,12 @@ Dot-only glob:
246
246
  - To lint every file in the current directory tree, the command "markdownlint-cli2 **" can be used instead
247
247
 
248
248
  Optional parameters:
249
- - --config specifies the path to a configuration file to define the base configuration
250
- - --fix updates files to resolve fixable issues (can be overridden in configuration)
251
- - --format reads standard input (stdin), applies fixes, writes standard output (stdout)
252
- - --help writes this message to the console and exits without doing anything else
253
- - --no-globs ignores the "globs" property if present in the top-level options object
249
+ - --config specifies the path to a configuration file to define the base configuration
250
+ - --configPointer specifies a JSON Pointer to a configuration object within the --config file
251
+ - --fix updates files to resolve fixable issues (can be overridden in configuration)
252
+ - --format reads standard input (stdin), applies fixes, writes standard output (stdout)
253
+ - --help writes this message to the console and exits without doing anything else
254
+ - --no-globs ignores the "globs" property if present in the top-level options object
254
255
 
255
256
  Configuration via:
256
257
  - .markdownlint-cli2.jsonc
@@ -259,7 +260,6 @@ Configuration via:
259
260
  - .markdownlint.jsonc or .markdownlint.json
260
261
  - .markdownlint.yaml or .markdownlint.yml
261
262
  - .markdownlint.cjs or .markdownlint.mjs
262
- - package.json
263
263
 
264
264
  Cross-platform compatibility:
265
265
  - UNIX and Windows shells expand globs according to different rules; quoting arguments is recommended
@@ -275,15 +275,61 @@ $ markdownlint-cli2 "**/*.md" "#node_modules"`
275
275
  return 2;
276
276
  };
277
277
 
278
+ // Helpers for getAndProcessDirInfo/handleFirstMatchingConfigurationFile
279
+ const readJsoncWrapper = (/** @type {ConfigurationHandlerParams} */ { file, fs }) => readJsonc(file, fs);
280
+ const readYamlWrapper = (/** @type {ConfigurationHandlerParams} */ { file, fs }) => readYaml(file, fs);
281
+ const readConfigWrapper = (/** @type {ConfigurationHandlerParams} */ { file, fs, parsers }) => readConfig(file, parsers, fs);
282
+ const importModuleWrapper = (/** @type {ConfigurationHandlerParams} */ { dir, file, noImport }) => importModule(dir, file, noImport);
283
+
284
+ /** @type {ConfigurationFileAndHandler[] } */
285
+ const optionsFiles = [
286
+ [ ".markdownlint-cli2.jsonc", readJsoncWrapper ],
287
+ [ ".markdownlint-cli2.yaml", readYamlWrapper ],
288
+ [ ".markdownlint-cli2.cjs", importModuleWrapper ],
289
+ [ ".markdownlint-cli2.mjs", importModuleWrapper ]
290
+ ];
291
+
292
+ /** @type {ConfigurationFileAndHandler[] } */
293
+ const configurationFiles = [
294
+ [ ".markdownlint.jsonc", readConfigWrapper ],
295
+ [ ".markdownlint.json", readConfigWrapper ],
296
+ [ ".markdownlint.yaml", readConfigWrapper ],
297
+ [ ".markdownlint.yml", readConfigWrapper ],
298
+ [ ".markdownlint.cjs", importModuleWrapper ],
299
+ [ ".markdownlint.mjs", importModuleWrapper ]
300
+ ];
301
+
302
+ /**
303
+ * Processes the first matching configuration file.
304
+ * @param {ExecutionContext} context Execution context.
305
+ * @param {ConfigurationFileAndHandler[]} fileAndHandlers List of configuration files and handlers.
306
+ * @param {string} dir Configuration file directory.
307
+ * @param {(file: string) => void} memoizeFile Function to memoize file name.
308
+ * @returns {Promise<any>} Configuration file content.
309
+ */
310
+ const processFirstMatchingConfigurationFile = (context, fileAndHandlers, dir, memoizeFile) => {
311
+ const { fs, noImport, parsers } = context;
312
+ return Promise.allSettled(
313
+ fileAndHandlers.map(([ name, handler ]) => {
314
+ const file = pathPosix.join(dir, name);
315
+ return fs.promises.access(file).then(() => [ file, handler ]);
316
+ })
317
+ ).
318
+ then((values) => {
319
+ /** @type {ConfigurationFileAndHandler} */
320
+ const [ file, handler ] = values.find((result) => (result.status === "fulfilled"))?.value || [ "[UNUSED]", noop ];
321
+ memoizeFile(file);
322
+ return handler({ dir, file, fs, noImport, parsers });
323
+ });
324
+ };
325
+
278
326
  // Get (creating if necessary) and process a directory's info object
279
327
  const getAndProcessDirInfo = (
280
- /** @type {FsLike} */ fs,
328
+ /** @type {ExecutionContext} */ context,
281
329
  /** @type {Task[]} */ tasks,
282
330
  /** @type {DirToDirInfo} */ dirToDirInfo,
283
331
  /** @type {string} */ dir,
284
- /** @type {string | null} */ relativeDir,
285
- /** @type {boolean} */ noImport,
286
- /** @type {boolean} */ allowPackageJson
332
+ /** @type {string | null} */ relativeDir
287
333
  ) => {
288
334
  // Create dirInfo
289
335
  let dirInfo = dirToDirInfo[dir];
@@ -293,106 +339,37 @@ const getAndProcessDirInfo = (
293
339
  relativeDir,
294
340
  "parent": null,
295
341
  "files": [],
296
- "markdownlintConfig": {},
297
- "markdownlintOptions": {}
342
+ "markdownlintConfig": null,
343
+ "markdownlintOptions": null
298
344
  };
299
345
  dirToDirInfo[dir] = dirInfo;
300
346
 
301
- // Load markdownlint-cli2 object(s)
302
- const markdownlintCli2Jsonc = pathPosix.join(dir, ".markdownlint-cli2.jsonc");
303
- const markdownlintCli2Yaml = pathPosix.join(dir, ".markdownlint-cli2.yaml");
304
- const markdownlintCli2Cjs = pathPosix.join(dir, ".markdownlint-cli2.cjs");
305
- const markdownlintCli2Mjs = pathPosix.join(dir, ".markdownlint-cli2.mjs");
306
- const packageJson = pathPosix.join(dir, "package.json");
307
- let file = "[UNKNOWN]";
308
- // eslint-disable-next-line no-return-assign
309
- const captureFile = (/** @type {string} */ f) => file = f;
347
+ let cli2File = "[UNKNOWN]";
310
348
  tasks.push(
311
- fs.promises.access(captureFile(markdownlintCli2Jsonc)).
312
- then(
313
- () => fs.promises.readFile(file, utf8).then(jsoncParse),
314
- () => fs.promises.access(captureFile(markdownlintCli2Yaml)).
315
- then(
316
- () => fs.promises.readFile(file, utf8).then(yamlParse),
317
- () => fs.promises.access(captureFile(markdownlintCli2Cjs)).
318
- then(
319
- () => importModule(dir, file, noImport),
320
- () => fs.promises.access(captureFile(markdownlintCli2Mjs)).
321
- then(
322
- () => importModule(dir, file, noImport),
323
- () => (allowPackageJson
324
- ? fs.promises.access(captureFile(packageJson))
325
- // eslint-disable-next-line prefer-promise-reject-errors
326
- : Promise.reject()
327
- ).
328
- then(
329
- () => fs.promises.
330
- readFile(file, utf8).
331
- then(jsoncParse).
332
- then((/** @type {any} */ obj) => obj[packageName]),
333
- noop
334
- )
335
- )
336
- )
337
- )
338
- ).
339
- then((/** @type {Options} */ options) => {
349
+
350
+ // Load markdownlint-cli2 object(s)
351
+ processFirstMatchingConfigurationFile(context, optionsFiles, dir, (file) => { cli2File = file; }).
352
+ then((/** @type {Options | null} */ options) => {
340
353
  dirInfo.markdownlintOptions = options;
341
354
  return options &&
342
355
  options.config &&
343
356
  getExtendedConfig(
357
+ context,
344
358
  options.config,
345
- // Just need to identify a file in the right directory
346
- markdownlintCli2Jsonc,
347
- fs
359
+ // Just need to identify the right directory
360
+ pathPosix.join(dir, utf8)
348
361
  ).
349
362
  then((config) => {
350
363
  options.config = config;
351
364
  });
352
365
  }).
353
366
  catch((/** @type {Error} */ error) => {
354
- throwForConfigurationFile(file, error);
355
- })
356
- );
367
+ throwForConfigurationFile(cli2File, error);
368
+ }),
357
369
 
358
- // Load markdownlint object(s)
359
- const readConfigs =
360
- readConfigFile(
361
- fs,
362
- dir,
363
- ".markdownlint.jsonc",
364
- readConfigFile(
365
- fs,
366
- dir,
367
- ".markdownlint.json",
368
- readConfigFile(
369
- fs,
370
- dir,
371
- ".markdownlint.yaml",
372
- readConfigFile(
373
- fs,
374
- dir,
375
- ".markdownlint.yml",
376
- importConfig(
377
- fs,
378
- dir,
379
- ".markdownlint.cjs",
380
- noImport,
381
- importConfig(
382
- fs,
383
- dir,
384
- ".markdownlint.mjs",
385
- noImport,
386
- noop
387
- )
388
- )
389
- )
390
- )
391
- )
392
- );
393
- tasks.push(
394
- readConfigs().
395
- then((/** @type {import("markdownlint").Configuration} */ config) => {
370
+ // Load markdownlint object(s)
371
+ processFirstMatchingConfigurationFile(context, configurationFiles, dir, noop).
372
+ then((/** @type {Configuration | null} */ config) => {
396
373
  dirInfo.markdownlintConfig = config;
397
374
  })
398
375
  );
@@ -404,27 +381,24 @@ const getAndProcessDirInfo = (
404
381
 
405
382
  // Get base markdownlint-cli2 options object
406
383
  const getBaseOptions = async (
407
- /** @type {FsLike} */ fs,
408
- /** @type {string} */ baseDir,
384
+ /** @type {ExecutionContext} */ context,
409
385
  /** @type {string | null} */ relativeDir,
410
386
  /** @type {string[]} */ globPatterns,
411
387
  /** @type {Options} */ options,
412
388
  /** @type {boolean} */ fixDefault,
413
- /** @type {boolean} */ noGlobs,
414
- /** @type {boolean} */ noImport
389
+ /** @type {boolean} */ noGlobs
415
390
  ) => {
391
+ const { baseDir } = context;
416
392
  /** @type {Task[]} */
417
393
  const tasks = [];
418
394
  /** @type {DirToDirInfo} */
419
395
  const dirToDirInfo = {};
420
396
  getAndProcessDirInfo(
421
- fs,
397
+ context,
422
398
  tasks,
423
399
  dirToDirInfo,
424
400
  baseDir,
425
- relativeDir,
426
- noImport,
427
- true
401
+ relativeDir
428
402
  );
429
403
  await Promise.all(tasks);
430
404
  // eslint-disable-next-line no-multi-assign
@@ -457,15 +431,13 @@ const getBaseOptions = async (
457
431
 
458
432
  // Enumerate files from globs and build directory infos
459
433
  const enumerateFiles = async (
460
- /** @type {FsLike} */ fs,
461
- /** @type {string} */ baseDirSystem,
462
- /** @type {string} */ baseDir,
434
+ /** @type {ExecutionContext} */ context,
463
435
  /** @type {string[]} */ globPatterns,
464
436
  /** @type {DirToDirInfo} */ dirToDirInfo,
465
437
  /** @type {boolean} */ gitignore,
466
- /** @type {string | undefined} */ ignoreFiles,
467
- /** @type {boolean} */ noImport
438
+ /** @type {string | undefined} */ ignoreFiles
468
439
  ) => {
440
+ const { baseDir, baseDirSystem, fs } = context;
469
441
  /** @type {Task[]} */
470
442
  const tasks = [];
471
443
  /** @type {import("globby").Options} */
@@ -473,7 +445,7 @@ const enumerateFiles = async (
473
445
  "absolute": true,
474
446
  "cwd": baseDir,
475
447
  "dot": true,
476
- "expandDirectories": false,
448
+ "expandNegationOnlyPatterns": false,
477
449
  gitignore,
478
450
  ignoreFiles,
479
451
  "suppressErrors": true,
@@ -501,41 +473,19 @@ const enumerateFiles = async (
501
473
  ((literalFiles.length > 0) && (globsForIgnore.length > 0))
502
474
  ? removeIgnoredFiles(baseDir, literalFiles, globsForIgnore)
503
475
  : literalFiles;
504
- // Manually expand directories to avoid globby call to dir-glob.sync
505
- const expandedDirectories = await Promise.all(
506
- filteredGlobPatterns.map((globPattern) => {
507
- const barePattern =
508
- globPattern.startsWith("!")
509
- ? globPattern.slice(1)
510
- : globPattern;
511
- const globPath = (
512
- pathPosix.isAbsolute(barePattern) ||
513
- pathDefault.isAbsolute(barePattern)
514
- )
515
- ? barePattern
516
- : pathPosix.join(baseDir, barePattern);
517
- return fs.promises.stat(globPath).
518
- then((/** @type {import("node:fs").Stats} */ stats) => (stats.isDirectory()
519
- ? pathPosix.join(globPattern, "**")
520
- : globPattern)).
521
- catch(() => globPattern);
522
- })
523
- );
524
476
  // Process glob patterns
525
477
  const files = [
526
- ...await globby(expandedDirectories, globbyOptions),
478
+ ...await globby(filteredGlobPatterns, globbyOptions),
527
479
  ...filteredLiteralFiles
528
480
  ];
529
481
  for (const file of files) {
530
482
  const dir = pathPosix.dirname(file);
531
483
  const dirInfo = getAndProcessDirInfo(
532
- fs,
484
+ context,
533
485
  tasks,
534
486
  dirToDirInfo,
535
487
  dir,
536
- null,
537
- noImport,
538
- false
488
+ null
539
489
  );
540
490
  dirInfo.files.push(file);
541
491
  }
@@ -544,11 +494,10 @@ const enumerateFiles = async (
544
494
 
545
495
  // Enumerate (possibly missing) parent directories and update directory infos
546
496
  const enumerateParents = async (
547
- /** @type {FsLike} */ fs,
548
- /** @type {string} */ baseDir,
549
- /** @type {DirToDirInfo} */ dirToDirInfo,
550
- /** @type {boolean} */ noImport
497
+ /** @type {ExecutionContext} */ context,
498
+ /** @type {DirToDirInfo} */ dirToDirInfo
551
499
  ) => {
500
+ const { baseDir } = context;
552
501
  /** @type {Task[]} */
553
502
  const tasks = [];
554
503
 
@@ -573,13 +522,11 @@ const enumerateParents = async (
573
522
  lastDir = dir;
574
523
  const dirInfo =
575
524
  getAndProcessDirInfo(
576
- fs,
525
+ context,
577
526
  tasks,
578
527
  dirToDirInfo,
579
528
  dir,
580
- null,
581
- noImport,
582
- false
529
+ null
583
530
  );
584
531
  lastDirInfo.parent = dirInfo;
585
532
  lastDirInfo = dirInfo;
@@ -595,31 +542,23 @@ const enumerateParents = async (
595
542
 
596
543
  // Create directory info objects by enumerating file globs
597
544
  const createDirInfos = async (
598
- /** @type {FsLike} */ fs,
599
- /** @type {string} */ baseDirSystem,
600
- /** @type {string} */ baseDir,
545
+ /** @type {ExecutionContext} */ context,
601
546
  /** @type {string[]} */ globPatterns,
602
547
  /** @type {DirToDirInfo} */ dirToDirInfo,
603
548
  /** @type {Options | undefined} */ optionsOverride,
604
549
  /** @type {boolean} */ gitignore,
605
- /** @type {string | undefined} */ ignoreFiles,
606
- /** @type {boolean} */ noImport
550
+ /** @type {string | undefined} */ ignoreFiles
607
551
  ) => {
608
552
  await enumerateFiles(
609
- fs,
610
- baseDirSystem,
611
- baseDir,
553
+ context,
612
554
  globPatterns,
613
555
  dirToDirInfo,
614
556
  gitignore,
615
- ignoreFiles,
616
- noImport
557
+ ignoreFiles
617
558
  );
618
559
  await enumerateParents(
619
- fs,
620
- baseDir,
621
- dirToDirInfo,
622
- noImport
560
+ context,
561
+ dirToDirInfo
623
562
  );
624
563
 
625
564
  // Merge file lists with identical configuration
@@ -635,6 +574,7 @@ const createDirInfos = async (
635
574
  }
636
575
  delete dirToDirInfo[dir];
637
576
  } else {
577
+ const { noImport } = context;
638
578
  const { markdownlintOptions, relativeDir } = dirInfo;
639
579
  const effectiveDir = relativeDir || dir;
640
580
  const effectiveModulePaths = resolveModulePaths(
@@ -704,8 +644,7 @@ const createDirInfos = async (
704
644
 
705
645
  // Merge configuration by inheritance
706
646
  for (const dirInfo of dirInfos) {
707
- let markdownlintOptions = dirInfo.markdownlintOptions || {};
708
- let { markdownlintConfig } = dirInfo;
647
+ let { markdownlintConfig, markdownlintOptions } = dirInfo;
709
648
  /** @type {DirInfo | null} */
710
649
  let parent = dirInfo;
711
650
  // eslint-disable-next-line prefer-destructuring
@@ -719,7 +658,7 @@ const createDirInfos = async (
719
658
  if (
720
659
  !markdownlintConfig &&
721
660
  parent.markdownlintConfig &&
722
- !markdownlintOptions.config
661
+ !markdownlintOptions?.config
723
662
  ) {
724
663
  // eslint-disable-next-line prefer-destructuring
725
664
  markdownlintConfig = parent.markdownlintConfig;
@@ -735,7 +674,13 @@ const createDirInfos = async (
735
674
  };
736
675
 
737
676
  // Lint files in groups by shared configuration
738
- const lintFiles = (/** @type {FsLike} */ fs, /** @type {DirInfo[]} */ dirInfos, /** @type {Record<string, string>} */ fileContents, /** @type {FormattingContext} */ formattingContext) => {
677
+ const lintFiles = (
678
+ /** @type {ExecutionContext} */ context,
679
+ /** @type {DirInfo[]} */ dirInfos,
680
+ /** @type {Record<string, string>} */ fileContents,
681
+ /** @type {FormattingContext} */ formattingContext
682
+ ) => {
683
+ const { fs, parsers } = context;
739
684
  const tasks = [];
740
685
  // For each dirInfo
741
686
  for (const dirInfo of dirInfos) {
@@ -743,6 +688,7 @@ const lintFiles = (/** @type {FsLike} */ fs, /** @type {DirInfo[]} */ dirInfos,
743
688
  // Filter file/string inputs to only those in the dirInfo
744
689
  let filesAfterIgnores = files;
745
690
  if (
691
+ markdownlintOptions &&
746
692
  markdownlintOptions.ignores &&
747
693
  (markdownlintOptions.ignores.length > 0)
748
694
  ) {
@@ -766,7 +712,7 @@ const lintFiles = (/** @type {FsLike} */ fs, /** @type {DirInfo[]} */ dirInfos,
766
712
  // eslint-disable-next-line no-inline-comments
767
713
  const module = await import(/* webpackMode: "eager" */ "markdown-it");
768
714
  const markdownIt = module.default({ "html": true });
769
- for (const plugin of (markdownlintOptions.markdownItPlugins || [])) {
715
+ for (const plugin of (markdownlintOptions?.markdownItPlugins || [])) {
770
716
  // @ts-ignore
771
717
  markdownIt.use(...plugin);
772
718
  }
@@ -777,16 +723,16 @@ const lintFiles = (/** @type {FsLike} */ fs, /** @type {DirInfo[]} */ dirInfos,
777
723
  const options = {
778
724
  "files": filteredFiles,
779
725
  "strings": filteredStrings,
780
- "config": markdownlintConfig || markdownlintOptions.config,
726
+ "config": markdownlintConfig || markdownlintOptions?.config,
781
727
  "configParsers": parsers,
782
728
  // @ts-ignore
783
729
  "customRules": markdownlintOptions.customRules,
784
- "frontMatter": markdownlintOptions.frontMatter
785
- ? new RegExp(markdownlintOptions.frontMatter, "u")
730
+ "frontMatter": markdownlintOptions?.frontMatter
731
+ ? new RegExp(markdownlintOptions?.frontMatter, "u")
786
732
  : undefined,
787
733
  "handleRuleFailures": true,
788
734
  markdownItFactory,
789
- "noInlineConfig": Boolean(markdownlintOptions.noInlineConfig),
735
+ "noInlineConfig": Boolean(markdownlintOptions?.noInlineConfig),
790
736
  fs
791
737
  };
792
738
  // Invoke markdownlint
@@ -799,7 +745,7 @@ const lintFiles = (/** @type {FsLike} */ fs, /** @type {DirInfo[]} */ dirInfos,
799
745
  formattingContext.formatted = applyFixes(original, errorInfos);
800
746
  return {};
801
747
  });
802
- } else if (markdownlintOptions.fix) {
748
+ } else if (markdownlintOptions?.fix) {
803
749
  // For any fixable errors, read file, apply fixes, write it back, and re-lint
804
750
  task = task.then((results) => {
805
751
  options.files = [];
@@ -836,7 +782,7 @@ const lintFiles = (/** @type {FsLike} */ fs, /** @type {DirInfo[]} */ dirInfos,
836
782
  };
837
783
 
838
784
  // Create list of results
839
- const createResults = (/** @type {string} */ baseDir, /** @type {import("markdownlint").LintResults[]} */ taskResults) => {
785
+ const createResults = (/** @type {string} */ baseDir, /** @type {LintResults[]} */ taskResults) => {
840
786
  /** @type {LintResult[]} */
841
787
  const results = [];
842
788
  /** @type {Map<LintResult, number>} */
@@ -907,7 +853,6 @@ export const main = async (/** @type {Parameters} */ params) => {
907
853
  optionsDefault,
908
854
  optionsOverride,
909
855
  fileContents,
910
- noImport,
911
856
  allowStdin
912
857
  } = params;
913
858
  let {
@@ -917,6 +862,7 @@ export const main = async (/** @type {Parameters} */ params) => {
917
862
  const logMessage = params.logMessage || noop;
918
863
  const logError = params.logError || noop;
919
864
  const fs = params.fs || fsNode;
865
+ const noImport = Boolean(params.noImport);
920
866
  const baseDirSystem =
921
867
  (directory && pathDefault.resolve(directory)) ||
922
868
  process.cwd();
@@ -928,6 +874,9 @@ export const main = async (/** @type {Parameters} */ params) => {
928
874
  /** @type {undefined | null | string} */
929
875
  // eslint-disable-next-line unicorn/no-useless-undefined
930
876
  let configPath = undefined;
877
+ /** @type {undefined | null | string} */
878
+ // eslint-disable-next-line unicorn/no-useless-undefined
879
+ let configPointer = undefined;
931
880
  let useStdin = false;
932
881
  let sawDashDash = false;
933
882
  let shouldShowHelp = false;
@@ -936,6 +885,8 @@ export const main = async (/** @type {Parameters} */ params) => {
936
885
  return true;
937
886
  } else if (configPath === null) {
938
887
  configPath = arg;
888
+ } else if (configPointer === null) {
889
+ configPointer = arg;
939
890
  } else if ((arg === "-") && allowStdin) {
940
891
  useStdin = true;
941
892
  // eslint-disable-next-line unicorn/prefer-switch
@@ -943,6 +894,8 @@ export const main = async (/** @type {Parameters} */ params) => {
943
894
  sawDashDash = true;
944
895
  } else if (arg === "--config") {
945
896
  configPath = null;
897
+ } else if (arg === "--configPointer") {
898
+ configPointer = null;
946
899
  } else if (arg === "--fix") {
947
900
  fixDefault = true;
948
901
  } else if (arg === "--format") {
@@ -960,6 +913,8 @@ export const main = async (/** @type {Parameters} */ params) => {
960
913
  if (shouldShowHelp) {
961
914
  return showHelp(logMessage, true);
962
915
  }
916
+ /** @type {ExecutionContext} */
917
+ const context = { baseDir, baseDirSystem, fs, noImport, "parsers": allParsers };
963
918
  // Read argv configuration file (if relevant and present)
964
919
  let optionsArgv = null;
965
920
  let relativeDir = null;
@@ -967,23 +922,19 @@ export const main = async (/** @type {Parameters} */ params) => {
967
922
  let baseOptions = null;
968
923
  try {
969
924
  if (configPath) {
970
- const resolvedConfigPath =
971
- posixPath(pathDefault.resolve(baseDirSystem, configPath));
972
- optionsArgv =
973
- await readOptionsOrConfig(resolvedConfigPath, fs, Boolean(noImport));
925
+ const resolvedConfigPath = posixPath(pathDefault.resolve(baseDirSystem, configPath));
926
+ optionsArgv = await readOptionsOrConfig(context, resolvedConfigPath, configPointer);
974
927
  relativeDir = pathPosix.dirname(resolvedConfigPath);
975
928
  }
976
929
  // Process arguments and get base options
977
930
  globPatterns = processArgv(argvFiltered);
978
931
  baseOptions = await getBaseOptions(
979
- fs,
980
- baseDir,
932
+ context,
981
933
  relativeDir,
982
934
  globPatterns,
983
935
  optionsArgv || optionsDefault,
984
936
  fixDefault,
985
- Boolean(noGlobs),
986
- Boolean(noImport)
937
+ Boolean(noGlobs)
987
938
  );
988
939
  } finally {
989
940
  if (!baseOptions?.baseMarkdownlintOptions.noBanner && !formattingContext.formatting) {
@@ -1026,24 +977,18 @@ export const main = async (/** @type {Parameters} */ params) => {
1026
977
  logMessage(`Finding: ${globPatterns.join(" ")}`);
1027
978
  }
1028
979
  // Create linting tasks
1029
- const gitignore =
1030
- // https://github.com/sindresorhus/globby/issues/265
1031
- (!params.fs && (baseMarkdownlintOptions.gitignore === true));
1032
- const ignoreFiles =
1033
- (!params.fs && (typeof baseMarkdownlintOptions.gitignore === "string"))
1034
- ? baseMarkdownlintOptions.gitignore
1035
- : undefined;
980
+ const gitignore = (baseMarkdownlintOptions.gitignore === true);
981
+ const ignoreFiles = (typeof baseMarkdownlintOptions.gitignore === "string")
982
+ ? baseMarkdownlintOptions.gitignore
983
+ : undefined;
1036
984
  const dirInfos =
1037
985
  await createDirInfos(
1038
- fs,
1039
- baseDirSystem,
1040
- baseDir,
986
+ context,
1041
987
  globPatterns,
1042
988
  dirToDirInfo,
1043
989
  optionsOverride,
1044
990
  gitignore,
1045
- ignoreFiles,
1046
- Boolean(noImport)
991
+ ignoreFiles
1047
992
  );
1048
993
  // Output linting status
1049
994
  if (showProgress) {
@@ -1060,7 +1005,7 @@ export const main = async (/** @type {Parameters} */ params) => {
1060
1005
  logMessage(`Linting: ${fileCount} file(s)`);
1061
1006
  }
1062
1007
  // Lint files
1063
- const lintResults = await lintFiles(fs, dirInfos, resolvedFileContents, formattingContext);
1008
+ const lintResults = await lintFiles(context, dirInfos, resolvedFileContents, formattingContext);
1064
1009
  // Output summary
1065
1010
  const results = createResults(baseDir, lintResults);
1066
1011
  if (showProgress) {
@@ -1106,6 +1051,15 @@ export const main = async (/** @type {Parameters} */ params) => {
1106
1051
 
1107
1052
  /** @typedef {Promise<any>} Task */
1108
1053
 
1054
+ /**
1055
+ * @typedef ExecutionContext
1056
+ * @property {string} baseDir Base directory (POSIX).
1057
+ * @property {string} baseDirSystem Base directory (non-POSIX).
1058
+ * @property {FsLike} fs File system object.
1059
+ * @property {boolean} noImport No import.
1060
+ * @property {ConfigurationParser[]} parsers Configuration file parsers.
1061
+ */
1062
+
1109
1063
  /**
1110
1064
  * @typedef Parameters
1111
1065
  * @property {boolean} [allowStdin] Allow stdin.
@@ -1122,14 +1076,29 @@ export const main = async (/** @type {Parameters} */ params) => {
1122
1076
  * @property {Options} [optionsOverride] Options override.
1123
1077
  */
1124
1078
 
1079
+ /** @typedef {import("markdownlint").Configuration} Configuration */
1080
+
1081
+ /** @typedef {import("markdownlint").ConfigurationParser} ConfigurationParser */
1082
+
1083
+ /**
1084
+ * @typedef ConfigurationHandlerParams
1085
+ * @property {string} dir Configuration file directory.
1086
+ * @property {string} file Configuration file.
1087
+ * @property {FsLike} fs File system object.
1088
+ * @property {ConfigurationParser[]} parsers Configuration file parsers.
1089
+ * @property {boolean} noImport No import.
1090
+ */
1091
+
1092
+ /** @typedef {[ string, (params: ConfigurationHandlerParams) => Promise<any> ] } ConfigurationFileAndHandler */
1093
+
1125
1094
  /**
1126
1095
  * @typedef DirInfo
1127
1096
  * @property {string} dir Directory.
1128
1097
  * @property {string | null} relativeDir Relative directory.
1129
1098
  * @property {DirInfo | null} parent Parent.
1130
1099
  * @property {string[]} files Files.
1131
- * @property {import("markdownlint").Configuration} markdownlintConfig Configuration.
1132
- * @property {Options} markdownlintOptions Options.
1100
+ * @property {Configuration | null} markdownlintConfig Configuration.
1101
+ * @property {Options | null} markdownlintOptions Options.
1133
1102
  */
1134
1103
 
1135
1104
  /** @typedef {Record<string, DirInfo>} DirToDirInfo */
@@ -1138,10 +1107,12 @@ export const main = async (/** @type {Parameters} */ params) => {
1138
1107
 
1139
1108
  /** @typedef {[string]} OutputFormatterConfiguration */
1140
1109
 
1110
+ /** @typedef {import("markdownlint").Rule} Rule */
1111
+
1141
1112
  /**
1142
1113
  * @typedef Options
1143
- * @property {import("markdownlint").Configuration} [config] Config.
1144
- * @property {import("markdownlint").Rule[] | string[]} [customRules] Custom rules.
1114
+ * @property {Configuration} [config] Config.
1115
+ * @property {Rule[] | string[]} [customRules] Custom rules.
1145
1116
  * @property {boolean} [fix] Fix.
1146
1117
  * @property {string} [frontMatter] Front matter.
1147
1118
  * @property {boolean | string} [gitignore] Git ignore.
@@ -1163,6 +1134,8 @@ export const main = async (/** @type {Parameters} */ params) => {
1163
1134
 
1164
1135
  /** @typedef {import("markdownlint").LintError & LintContext} LintResult */
1165
1136
 
1137
+ /** @typedef {import("markdownlint").LintResults} LintResults */
1138
+
1166
1139
  /**
1167
1140
  * @typedef FormattingContext
1168
1141
  * @property {boolean} [formatting] True iff formatting.