diff --git a/README.md b/README.md index a871211c..a4644bd5 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ Glob expressions (from the globby library): - {} allows for a comma-separated list of "or" expressions - ! or # at the beginning of a pattern negate the match - : at the beginning identifies a literal file path +- - as a glob represents standard input (stdin) Dot-only glob: - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended diff --git a/markdownlint-cli2.js b/markdownlint-cli2.js index 3f8299e7..6327fa14 100755 --- a/markdownlint-cli2.js +++ b/markdownlint-cli2.js @@ -263,6 +263,7 @@ Glob expressions (from the globby library): - {} allows for a comma-separated list of "or" expressions - ! or # at the beginning of a pattern negate the match - : at the beginning identifies a literal file path +- - as a glob represents standard input (stdin) Dot-only glob: - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended @@ -807,7 +808,8 @@ const lintFiles = (fs, dirInfos, fileContents) => { task = task.then((results) => { options.files = []; const subTasks = []; - const errorFiles = Object.keys(results); + const errorFiles = Object.keys(results). + filter((result) => filteredFiles.includes(result)); for (const fileName of errorFiles) { const errorInfos = results[fileName]. filter((errorInfo) => errorInfo.fixInfo); @@ -909,11 +911,12 @@ const main = async (params) => { optionsDefault, optionsOverride, fileContents, - nonFileContents, - noRequire + noRequire, + allowStdin } = params; let { - noGlobs + noGlobs, + nonFileContents } = params; const logMessage = params.logMessage || noop; const logError = params.logError || noop; @@ -926,6 +929,7 @@ const main = async (params) => { let fixDefault = false; // eslint-disable-next-line unicorn/no-useless-undefined let configPath = undefined; + let useStdin = false; let sawDashDash = false; let shouldShowHelp = false; const argvFiltered = (argv || []).filter((arg) => { @@ -933,6 +937,8 @@ const main = async (params) => { return true; } else if (configPath === null) { configPath = arg; + } else if ((arg === "-") && allowStdin) { + useStdin = true; // eslint-disable-next-line unicorn/prefer-switch } else if (arg === "--") { sawDashDash = true; @@ -983,22 +989,30 @@ const main = async (params) => { } } if ( - ((globPatterns.length === 0) && !nonFileContents) || + ((globPatterns.length === 0) && !useStdin && !nonFileContents) || (configPath === null) ) { return showHelp(logMessage, false); } + // Add stdin as a non-file input if necessary + if (useStdin) { + const key = pathPosix.join(baseDir, "stdin"); + const { text } = require("node:stream/consumers"); + nonFileContents = { + ...nonFileContents, + [key]: await text(process.stdin) + }; + } // Include any file overrides or non-file content - const { baseMarkdownlintOptions, dirToDirInfo } = baseOptions; const resolvedFileContents = {}; for (const file in fileContents) { const resolvedFile = posixPath(pathDefault.resolve(baseDirSystem, file)); - resolvedFileContents[resolvedFile] = - fileContents[file]; + resolvedFileContents[resolvedFile] = fileContents[file]; } for (const nonFile in nonFileContents) { resolvedFileContents[nonFile] = nonFileContents[nonFile]; } + const { baseMarkdownlintOptions, dirToDirInfo } = baseOptions; appendToArray( dirToDirInfo[baseDir].files, Object.keys(nonFileContents || {}) @@ -1079,7 +1093,8 @@ const run = (overrides, args) => { const defaultParams = { "argv": argsAndArgv, "logMessage": console.log, - "logError": console.error + "logError": console.error, + "allowStdin": true }; const params = { ...defaultParams, diff --git a/test/markdownlint-cli2-test-exec.js b/test/markdownlint-cli2-test-exec.js index 95c1573d..394262f5 100644 --- a/test/markdownlint-cli2-test-exec.js +++ b/test/markdownlint-cli2-test-exec.js @@ -4,15 +4,19 @@ const fs = require("node:fs/promises"); const path = require("node:path"); +const test = require("ava").default; const testCases = require("./markdownlint-cli2-test-cases"); +const absolute = (rootDir, file) => path.join(rootDir, file); +const repositoryPath = (name) => path.join(__dirname, "..", name); + const invoke = (directory, args, noRequire, env, script) => async () => { await fs.access(directory); const { "default": spawn } = await import("nano-spawn"); return spawn( "node", [ - path.join(__dirname, "..", script || "markdownlint-cli2.js"), + repositoryPath(script || "markdownlint-cli2.js"), ...args ], { @@ -27,8 +31,6 @@ const invoke = (directory, args, noRequire, env, script) => async () => { catch((error) => error); }; -const absolute = (rootDir, file) => path.join(rootDir, file); - testCases({ "host": "exec", invoke, @@ -39,3 +41,151 @@ testCases({ "includeRequire": true, "includeAbsolute": true }); + +const invokeStdin = async (args, stdin, cwd) => { + const { "default": spawn } = await import("nano-spawn"); + return spawn( + "node", + [ + repositoryPath("markdownlint-cli2.js"), + ...args + ], + { + cwd, + "stdin": { "string": stdin } + } + ); +}; + +const validInput = "# Heading\n\nText\n"; +const invalidInput = "# Heading\n\nText"; + +test("- parameter with empty input from stdin", (t) => { + t.plan(1); + return invokeStdin( + [ "-" ], + "" + ). + then(() => t.pass()). + catch(() => t.fail()); +}); + +test("- parameter with valid input from stdin", (t) => { + t.plan(1); + return invokeStdin( + [ "-" ], + validInput + ). + then(() => t.pass()). + catch(() => t.fail()); +}); + +test("- parameter with invalid input from stdin", (t) => { + t.plan(2); + return invokeStdin( + [ "-" ], + invalidInput + ). + then(() => t.fail()). + catch((error) => { + t.is(error.exitCode, 1); + t.is("", error.stderr.replace(/^stdin:1:3 MD019\/.*$[\n\r]+^stdin:3:4 MD047\/.*$/mu, "")); + }); +}); + +test("- parameter with invalid input from stdin and --fix", (t) => { + t.plan(2); + return invokeStdin( + [ "-", "--fix" ], + invalidInput + ). + then(() => t.fail()). + catch((error) => { + t.is(error.exitCode, 1); + t.is("", error.stderr.replace(/^stdin:1:3 MD019\/.*$[\n\r]+^stdin:3:4 MD047\/.*$/mu, "")); + }); +}); + +test("- parameter multiple times with invalid input", (t) => { + t.plan(2); + return invokeStdin( + [ "-", "-" ], + invalidInput + ). + then(() => t.fail()). + catch((error) => { + t.is(error.exitCode, 1); + t.is("", error.stderr.replace(/^stdin:1:3 MD019\/.*$[\n\r]+^stdin:3:4 MD047\/.*$/mu, "")); + }); +}); + +test("- parameter with valid input combined with valid globs", (t) => { + t.plan(1); + return invokeStdin( + [ repositoryPath("CONTRIBUTING.md"), "-", repositoryPath("README.md") ], + validInput + ). + then(() => t.pass()). + catch(() => t.fail()); +}); + +test("- parameter with invalid input combined with valid globs", (t) => { + t.plan(2); + return invokeStdin( + [ repositoryPath("CONTRIBUTING.md"), repositoryPath("README.md"), "-" ], + invalidInput + ). + then(() => t.fail()). + catch((error) => { + t.is(error.exitCode, 1); + t.is("", error.stderr.replace(/^stdin:1:3 MD019\/.*$[\n\r]+^stdin:3:4 MD047\/.*$/mu, "")); + }); +}); + +test("- parameter with invalid input combined with invalid glob", (t) => { + t.plan(2); + return invokeStdin( + [ "-", repositoryPath("LICENSE") ], + invalidInput + ). + then(() => t.fail()). + catch((error) => { + t.is(error.exitCode, 1); + t.is("", error.stderr.replace(/^LICENSE:1 MD041\/.*$[\n\r]+^stdin:1:3 MD019\/.*$[\n\r]+^stdin:3:4 MD047\/.*$/mu, "")); + }); +}); + +test("- parameter uses base directory configuration", (t) => { + t.plan(2); + return invokeStdin( + [ "-" ], + invalidInput, + path.join(__dirname, "stdin") + ). + then(() => t.fail()). + catch((error) => { + t.is(error.exitCode, 1); + t.is("", error.stderr.replace(/^stdin:1:3 MD019\/.*$/mu, "")); + }); +}); + +test("- parameter not treated as stdin in configuration file globs", (t) => { + t.plan(1); + return invokeStdin( + [], + invalidInput, + path.join(__dirname, "stdin-globs") + ). + then(() => t.pass()). + catch(() => t.fail()); +}); + +test("- parameter ignored after --", (t) => { + t.plan(1); + return invokeStdin( + [ "--", "-" ], + invalidInput + ). + then(() => t.pass()). + catch(() => t.fail()); +}); diff --git a/test/markdownlint-cli2-test.js b/test/markdownlint-cli2-test.js index 6f61afa2..b08503ca 100644 --- a/test/markdownlint-cli2-test.js +++ b/test/markdownlint-cli2-test.js @@ -67,7 +67,7 @@ test("README files", (t) => { }); test("validateMarkdownlintConfigSchema", async (t) => { - t.plan(26); + t.plan(27); // Validate schema // @ts-ignore @@ -111,7 +111,7 @@ test("validateMarkdownlintConfigSchema", async (t) => { }); test("validateMarkdownlintCli2ConfigSchema", async (t) => { - t.plan(90); + t.plan(91); // Validate schema // @ts-ignore @@ -696,3 +696,16 @@ test("-- stops matching parameters per POSIX Utility Conventions 12.2 Guideline files.push([ "/--", "# Title" ]); await scenario([ "--", "--" ], 1); }); + +test ("- not supported by main entry point", (t) => { + t.plan(2); + return markdownlintCli2({ + "argv": [ "-" ], + "optionsOverride": { + "outputFormatters": [ [ outputFormatterLengthIs(t, 0) ] ] + } + }). + then((exitCode) => { + t.is(exitCode, 0); + }); +}); diff --git a/test/snapshots/markdownlint-cli2-test-exec.js.md b/test/snapshots/markdownlint-cli2-test-exec.js.md index 639f0ae4..c6b5e6e6 100644 --- a/test/snapshots/markdownlint-cli2-test-exec.js.md +++ b/test/snapshots/markdownlint-cli2-test-exec.js.md @@ -27,6 +27,7 @@ Generated by [AVA](https://avajs.dev). - {} allows for a comma-separated list of "or" expressions␊ - ! or # at the beginning of a pattern negate the match␊ - : at the beginning identifies a literal file path␊ + - - as a glob represents standard input (stdin)␊ ␊ Dot-only glob:␊ - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊ @@ -83,6 +84,7 @@ Generated by [AVA](https://avajs.dev). - {} allows for a comma-separated list of "or" expressions␊ - ! or # at the beginning of a pattern negate the match␊ - : at the beginning identifies a literal file path␊ + - - as a glob represents standard input (stdin)␊ ␊ Dot-only glob:␊ - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊ @@ -139,6 +141,7 @@ Generated by [AVA](https://avajs.dev). - {} allows for a comma-separated list of "or" expressions␊ - ! or # at the beginning of a pattern negate the match␊ - : at the beginning identifies a literal file path␊ + - - as a glob represents standard input (stdin)␊ ␊ Dot-only glob:␊ - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊ @@ -195,6 +198,7 @@ Generated by [AVA](https://avajs.dev). - {} allows for a comma-separated list of "or" expressions␊ - ! or # at the beginning of a pattern negate the match␊ - : at the beginning identifies a literal file path␊ + - - as a glob represents standard input (stdin)␊ ␊ Dot-only glob:␊ - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊ @@ -681,6 +685,7 @@ Generated by [AVA](https://avajs.dev). - {} allows for a comma-separated list of "or" expressions␊ - ! or # at the beginning of a pattern negate the match␊ - : at the beginning identifies a literal file path␊ + - - as a glob represents standard input (stdin)␊ ␊ Dot-only glob:␊ - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊ diff --git a/test/snapshots/markdownlint-cli2-test-exec.js.snap b/test/snapshots/markdownlint-cli2-test-exec.js.snap index 1eb224b0..fed06ee8 100644 Binary files a/test/snapshots/markdownlint-cli2-test-exec.js.snap and b/test/snapshots/markdownlint-cli2-test-exec.js.snap differ diff --git a/test/snapshots/markdownlint-cli2-test-fs.js.md b/test/snapshots/markdownlint-cli2-test-fs.js.md index 671c6f8f..47abcbb8 100644 --- a/test/snapshots/markdownlint-cli2-test-fs.js.md +++ b/test/snapshots/markdownlint-cli2-test-fs.js.md @@ -27,6 +27,7 @@ Generated by [AVA](https://avajs.dev). - {} allows for a comma-separated list of "or" expressions␊ - ! or # at the beginning of a pattern negate the match␊ - : at the beginning identifies a literal file path␊ + - - as a glob represents standard input (stdin)␊ ␊ Dot-only glob:␊ - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊ @@ -84,6 +85,7 @@ Generated by [AVA](https://avajs.dev). - {} allows for a comma-separated list of "or" expressions␊ - ! or # at the beginning of a pattern negate the match␊ - : at the beginning identifies a literal file path␊ + - - as a glob represents standard input (stdin)␊ ␊ Dot-only glob:␊ - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊ @@ -141,6 +143,7 @@ Generated by [AVA](https://avajs.dev). - {} allows for a comma-separated list of "or" expressions␊ - ! or # at the beginning of a pattern negate the match␊ - : at the beginning identifies a literal file path␊ + - - as a glob represents standard input (stdin)␊ ␊ Dot-only glob:␊ - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊ @@ -198,6 +201,7 @@ Generated by [AVA](https://avajs.dev). - {} allows for a comma-separated list of "or" expressions␊ - ! or # at the beginning of a pattern negate the match␊ - : at the beginning identifies a literal file path␊ + - - as a glob represents standard input (stdin)␊ ␊ Dot-only glob:␊ - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊ @@ -711,6 +715,7 @@ Generated by [AVA](https://avajs.dev). - {} allows for a comma-separated list of "or" expressions␊ - ! or # at the beginning of a pattern negate the match␊ - : at the beginning identifies a literal file path␊ + - - as a glob represents standard input (stdin)␊ ␊ Dot-only glob:␊ - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊ diff --git a/test/snapshots/markdownlint-cli2-test-fs.js.snap b/test/snapshots/markdownlint-cli2-test-fs.js.snap index a663aaeb..ad8442e0 100644 Binary files a/test/snapshots/markdownlint-cli2-test-fs.js.snap and b/test/snapshots/markdownlint-cli2-test-fs.js.snap differ diff --git a/test/snapshots/markdownlint-cli2-test-main.js.md b/test/snapshots/markdownlint-cli2-test-main.js.md index 5e7236b9..c76a91bd 100644 --- a/test/snapshots/markdownlint-cli2-test-main.js.md +++ b/test/snapshots/markdownlint-cli2-test-main.js.md @@ -27,6 +27,7 @@ Generated by [AVA](https://avajs.dev). - {} allows for a comma-separated list of "or" expressions␊ - ! or # at the beginning of a pattern negate the match␊ - : at the beginning identifies a literal file path␊ + - - as a glob represents standard input (stdin)␊ ␊ Dot-only glob:␊ - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊ @@ -84,6 +85,7 @@ Generated by [AVA](https://avajs.dev). - {} allows for a comma-separated list of "or" expressions␊ - ! or # at the beginning of a pattern negate the match␊ - : at the beginning identifies a literal file path␊ + - - as a glob represents standard input (stdin)␊ ␊ Dot-only glob:␊ - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊ @@ -141,6 +143,7 @@ Generated by [AVA](https://avajs.dev). - {} allows for a comma-separated list of "or" expressions␊ - ! or # at the beginning of a pattern negate the match␊ - : at the beginning identifies a literal file path␊ + - - as a glob represents standard input (stdin)␊ ␊ Dot-only glob:␊ - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊ @@ -198,6 +201,7 @@ Generated by [AVA](https://avajs.dev). - {} allows for a comma-separated list of "or" expressions␊ - ! or # at the beginning of a pattern negate the match␊ - : at the beginning identifies a literal file path␊ + - - as a glob represents standard input (stdin)␊ ␊ Dot-only glob:␊ - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊ @@ -711,6 +715,7 @@ Generated by [AVA](https://avajs.dev). - {} allows for a comma-separated list of "or" expressions␊ - ! or # at the beginning of a pattern negate the match␊ - : at the beginning identifies a literal file path␊ + - - as a glob represents standard input (stdin)␊ ␊ Dot-only glob:␊ - The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊ diff --git a/test/snapshots/markdownlint-cli2-test-main.js.snap b/test/snapshots/markdownlint-cli2-test-main.js.snap index 84839101..fe8d01fb 100644 Binary files a/test/snapshots/markdownlint-cli2-test-main.js.snap and b/test/snapshots/markdownlint-cli2-test-main.js.snap differ diff --git a/test/stdin-globs/.markdownlint-cli2.jsonc b/test/stdin-globs/.markdownlint-cli2.jsonc new file mode 100644 index 00000000..5a9c315b --- /dev/null +++ b/test/stdin-globs/.markdownlint-cli2.jsonc @@ -0,0 +1,9 @@ +{ + "config": { + "single-trailing-newline": false + }, + "globs": [ + "*.md", + "-" + ] +} diff --git a/test/stdin-globs/viewme.md b/test/stdin-globs/viewme.md new file mode 100644 index 00000000..dd663662 --- /dev/null +++ b/test/stdin-globs/viewme.md @@ -0,0 +1,13 @@ +# Title + +> Tagline + +## Description + +Text text text +Text text text +Text text text + +## Summary + +Text text text diff --git a/test/stdin/.markdownlint.jsonc b/test/stdin/.markdownlint.jsonc new file mode 100644 index 00000000..35c7d6f6 --- /dev/null +++ b/test/stdin/.markdownlint.jsonc @@ -0,0 +1,3 @@ +{ + "single-trailing-newline": false +} diff --git a/webworker/stream-promises.js b/webworker/module-empty.js similarity index 100% rename from webworker/stream-promises.js rename to webworker/module-empty.js diff --git a/webworker/url-stub.js b/webworker/url-stub.js deleted file mode 100644 index 6d30054f..00000000 --- a/webworker/url-stub.js +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-check - -"use strict"; - -module.exports = {}; diff --git a/webworker/webpack.config.js b/webworker/webpack.config.js index 69935fb7..2fcc75ee 100644 --- a/webworker/webpack.config.js +++ b/webworker/webpack.config.js @@ -27,11 +27,11 @@ module.exports = { resource.request = module; } ), - // Intercept "node:stream/promises" lacking a browserify entry + // Intercept "node:stream/consumers" and "node:stream/promises" lacking a browserify entry new webpack.NormalModuleReplacementPlugin( - /^stream\/promises$/u, + /^stream\/(?:consumers|promises)$/u, (resource) => { - resource.request = require.resolve("./stream-promises.js"); + resource.request = require.resolve("./module-empty.js"); } ), // Intercept existing "unicorn-magic" package to provide missing import @@ -55,7 +55,7 @@ module.exports = { "process": require.resolve("./process-stub.js"), "process-wrapper": require.resolve("./process-stub.js"), "stream": require.resolve("stream-browserify"), - "url": require.resolve("./url-stub.js") + "url": require.resolve("./module-empty.js") } } };