diff --git a/.eslintrc.json b/.eslintrc.json index 86878e12..b8fd627b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -39,7 +39,7 @@ "max-depth": "off", "max-lines": "off", "max-lines-per-function": "off", - "max-params": ["error", 10], + "max-params": ["off"], "max-statements": "off", "multiline-comment-style": ["error", "separate-lines"], "multiline-ternary": "off", diff --git a/README.md b/README.md index 189381ec..1054ef62 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,8 @@ function markdownlint(options) { ... } Type: `Object` -Configures the function. +Configures the function. All properties are optional, but at least one +of `files` or `strings` should be set to provide input. ##### options.customRules @@ -534,6 +535,16 @@ Each item in the top-level `Array` should be of the form: [ require("markdown-it-plugin"), plugin_param_0, plugin_param_1, ... ] ``` +##### options.fs + +Type: `Object` implementing the [file system API](https://nodejs.org/api/fs.html) + +In advanced scenarios, it may be desirable to bypass the default file system API. +If a custom file system implementation is provided, `markdownlint` will use that +instead of invoking `require("fs")`. + +Note: The only methods called are `readFile` and `readFileSync`. + #### callback Type: `Function` taking (`Error`, `Object`) @@ -566,10 +577,11 @@ Asynchronous API: * * @param {string} file Configuration file name. * @param {ConfigurationParser[] | ReadConfigCallback} parsers Parsing function(s). + * @param {Object} [fs] File system implementation. * @param {ReadConfigCallback} [callback] Callback (err, result) function. * @returns {void} */ -function readConfig(file, parsers, callback) { ... } +function readConfig(file, parsers, fs, callback) { ... } ``` Synchronous API: @@ -580,13 +592,14 @@ Synchronous API: * * @param {string} file Configuration file name. * @param {ConfigurationParser[]} [parsers] Parsing function(s). + * @param {Object} [fs] File system implementation. * @returns {Configuration} Configuration object. */ -function readConfigSync(file, parsers) { ... } +function readConfigSync(file, parsers, fs) { ... } ``` Promise API (in the `promises` namespace like Node.js's -[`fs` Promises API](https://nodejs.org/api/fs.html#fs_fs_promises_api)): +[`fs` Promises API](https://nodejs.org/api/fs.html#fs_promises_api)): ```js /** @@ -594,9 +607,10 @@ Promise API (in the `promises` namespace like Node.js's * * @param {string} file Configuration file name. * @param {ConfigurationParser[]} [parsers] Parsing function(s). + * @param {Object} [fs] File system implementation. * @returns {Promise} Configuration object. */ -function readConfig(file, parsers) { ... } +function readConfig(file, parsers, fs) { ... } ``` #### file @@ -627,6 +641,16 @@ For example: [ JSON.parse, require("toml").parse, require("js-yaml").load ] ``` +#### fs + +Type: *Optional* `Object` implementing the [file system API](https://nodejs.org/api/fs.html) + +In advanced scenarios, it may be desirable to bypass the default file system API. +If a custom file system implementation is provided, `markdownlint` will use that +instead of invoking `require("fs")`. + +Note: The only methods called are `readFile`, `readFileSync`, and `accessSync`. + #### callback Type: `Function` taking (`Error`, `Object`) diff --git a/demo/markdownlint-browser.js b/demo/markdownlint-browser.js index 40ab0e0b..5080492b 100644 --- a/demo/markdownlint-browser.js +++ b/demo/markdownlint-browser.js @@ -813,7 +813,6 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from) { to[j] = from[i]; return to; }; -var fs = __webpack_require__(/*! fs */ "?ec0a"); var path = __webpack_require__(/*! path */ "?b85c"); var promisify = __webpack_require__(/*! util */ "?96a2").promisify; var markdownIt = __webpack_require__(/*! markdown-it */ "markdown-it"); @@ -1446,11 +1445,12 @@ function lintContent(ruleList, name, content, md, config, frontMatter, handleRul * @param {boolean} handleRuleFailures Whether to handle exceptions in rules. * @param {boolean} noInlineConfig Whether to allow inline configuration. * @param {number} resultVersion Version of the LintResults object to return. + * @param {Object} fs File system implementation. * @param {boolean} synchronous Whether to execute synchronously. * @param {Function} callback Callback (err, result) function. * @returns {void} */ -function lintFile(ruleList, file, md, config, frontMatter, handleRuleFailures, noInlineConfig, resultVersion, synchronous, callback) { +function lintFile(ruleList, file, md, config, frontMatter, handleRuleFailures, noInlineConfig, resultVersion, fs, synchronous, callback) { // eslint-disable-next-line jsdoc/require-jsdoc function lintContentWrapper(err, content) { if (err) { @@ -1460,7 +1460,6 @@ function lintFile(ruleList, file, md, config, frontMatter, handleRuleFailures, n } // Make a/synchronous call to read file if (synchronous) { - // @ts-ignore lintContentWrapper(null, fs.readFileSync(file, "utf8")); } else { @@ -1507,6 +1506,7 @@ function lintInput(options, synchronous, callback) { // @ts-ignore md.use.apply(md, plugin); }); + var fs = options.fs || __webpack_require__(/*! fs */ "?ec0a"); var results = newResults(ruleList); var done = false; // Linting of strings is always synchronous @@ -1526,7 +1526,7 @@ function lintInput(options, synchronous, callback) { if (synchronous) { // Lint files synchronously while (!done && (syncItem = files.shift())) { - lintFile(ruleList, syncItem, md, config, frontMatter, handleRuleFailures, noInlineConfig, resultVersion, synchronous, syncCallback); + lintFile(ruleList, syncItem, md, config, frontMatter, handleRuleFailures, noInlineConfig, resultVersion, fs, synchronous, syncCallback); } return done || callback(null, results); } @@ -1540,7 +1540,7 @@ function lintInput(options, synchronous, callback) { } else if (asyncItem) { concurrency++; - lintFile(ruleList, asyncItem, md, config, frontMatter, handleRuleFailures, noInlineConfig, resultVersion, synchronous, function (err, result) { + lintFile(ruleList, asyncItem, md, config, frontMatter, handleRuleFailures, noInlineConfig, resultVersion, fs, synchronous, function (err, result) { concurrency--; if (err) { done = true; @@ -1644,24 +1644,24 @@ function parseConfiguration(name, content, parsers) { * * @param {string} configFile Configuration file name. * @param {string} referenceId Referenced identifier to resolve. + * @param {Object} fs File system implementation. * @returns {string} Resolved path to file. */ -function resolveConfigExtends(configFile, referenceId) { +function resolveConfigExtends(configFile, referenceId, fs) { var configFileDirname = path.dirname(configFile); var resolvedExtendsFile = path.resolve(configFileDirname, referenceId); try { - if (fs.statSync(resolvedExtendsFile).isFile()) { - return resolvedExtendsFile; - } + fs.accessSync(resolvedExtendsFile); + return resolvedExtendsFile; } catch (_a) { - // If not a file or fs.statSync throws, try require.resolve + // Not a file, try require.resolve } try { return dynamicRequire.resolve(referenceId, { "paths": [configFileDirname] }); } catch (_b) { - // If require.resolve throws, return resolvedExtendsFile + // Unable to resolve, return resolvedExtendsFile } return resolvedExtendsFile; } @@ -1671,14 +1671,24 @@ function resolveConfigExtends(configFile, referenceId) { * @param {string} file Configuration file name. * @param {ConfigurationParser[] | ReadConfigCallback} parsers Parsing * function(s). + * @param {Object} [fs] File system implementation. * @param {ReadConfigCallback} [callback] Callback (err, result) function. * @returns {void} */ -function readConfig(file, parsers, callback) { +function readConfig(file, parsers, fs, callback) { if (!callback) { - // @ts-ignore - callback = parsers; - parsers = null; + if (fs) { + callback = fs; + fs = null; + } + else { + // @ts-ignore + callback = parsers; + parsers = null; + } + } + if (!fs) { + fs = __webpack_require__(/*! fs */ "?ec0a"); } // Read file fs.readFile(file, "utf8", function (err, content) { @@ -1695,8 +1705,8 @@ function readConfig(file, parsers, callback) { var configExtends = config["extends"]; if (configExtends) { delete config["extends"]; - var resolvedExtends = resolveConfigExtends(file, configExtends); - return readConfig(resolvedExtends, parsers, function (errr, extendsConfig) { + var resolvedExtends = resolveConfigExtends(file, configExtends, fs); + return readConfig(resolvedExtends, parsers, fs, function (errr, extendsConfig) { if (errr) { return callback(errr); } @@ -1712,22 +1722,26 @@ var readConfigPromisify = promisify && promisify(readConfig); * * @param {string} file Configuration file name. * @param {ConfigurationParser[]} [parsers] Parsing function(s). + * @param {Object} [fs] File system implementation. * @returns {Promise} Configuration object. */ -function readConfigPromise(file, parsers) { +function readConfigPromise(file, parsers, fs) { // @ts-ignore - return readConfigPromisify(file, parsers); + return readConfigPromisify(file, parsers, fs); } /** * Read specified configuration file synchronously. * * @param {string} file Configuration file name. * @param {ConfigurationParser[]} [parsers] Parsing function(s). + * @param {Object} [fs] File system implementation. * @returns {Configuration} Configuration object. */ -function readConfigSync(file, parsers) { +function readConfigSync(file, parsers, fs) { + if (!fs) { + fs = __webpack_require__(/*! fs */ "?ec0a"); + } // Read file - // @ts-ignore var content = fs.readFileSync(file, "utf8"); // Try to parse file var _a = parseConfiguration(file, content, parsers), config = _a.config, message = _a.message; @@ -1738,8 +1752,8 @@ function readConfigSync(file, parsers) { var configExtends = config["extends"]; if (configExtends) { delete config["extends"]; - var resolvedExtends = resolveConfigExtends(file, configExtends); - return __assign(__assign({}, readConfigSync(resolvedExtends, parsers)), config); + var resolvedExtends = resolveConfigExtends(file, configExtends, fs); + return __assign(__assign({}, readConfigSync(resolvedExtends, parsers, fs)), config); } return config; } diff --git a/lib/markdownlint.d.ts b/lib/markdownlint.d.ts index e344bd0d..be83dd54 100644 --- a/lib/markdownlint.d.ts +++ b/lib/markdownlint.d.ts @@ -52,6 +52,10 @@ type Options = { * Additional plugins. */ markdownItPlugins?: Plugin[]; + /** + * File system implementation. + */ + fs?: any; }; /** * Called with the result of the lint operation. @@ -70,18 +74,20 @@ declare function markdownlintSync(options: Options): LintResults; * @param {string} file Configuration file name. * @param {ConfigurationParser[] | ReadConfigCallback} parsers Parsing * function(s). + * @param {Object} [fs] File system implementation. * @param {ReadConfigCallback} [callback] Callback (err, result) function. * @returns {void} */ -declare function readConfig(file: string, parsers: ConfigurationParser[] | ReadConfigCallback, callback?: ReadConfigCallback): void; +declare function readConfig(file: string, parsers: ConfigurationParser[] | ReadConfigCallback, fs?: any, callback?: ReadConfigCallback): void; /** * Read specified configuration file synchronously. * * @param {string} file Configuration file name. * @param {ConfigurationParser[]} [parsers] Parsing function(s). + * @param {Object} [fs] File system implementation. * @returns {Configuration} Configuration object. */ -declare function readConfigSync(file: string, parsers?: ConfigurationParser[]): Configuration; +declare function readConfigSync(file: string, parsers?: ConfigurationParser[], fs?: any): Configuration; /** * Gets the (semantic) version of the library. * @@ -364,6 +370,7 @@ declare function markdownlintPromise(options: Options): Promise; * * @param {string} file Configuration file name. * @param {ConfigurationParser[]} [parsers] Parsing function(s). + * @param {Object} [fs] File system implementation. * @returns {Promise} Configuration object. */ -declare function readConfigPromise(file: string, parsers?: ConfigurationParser[]): Promise; +declare function readConfigPromise(file: string, parsers?: ConfigurationParser[], fs?: any): Promise; diff --git a/lib/markdownlint.js b/lib/markdownlint.js index b4cb77d5..dab39ed1 100644 --- a/lib/markdownlint.js +++ b/lib/markdownlint.js @@ -2,7 +2,6 @@ "use strict"; -const fs = require("fs"); const path = require("path"); const { promisify } = require("util"); const markdownIt = require("markdown-it"); @@ -685,6 +684,7 @@ function lintContent( * @param {boolean} handleRuleFailures Whether to handle exceptions in rules. * @param {boolean} noInlineConfig Whether to allow inline configuration. * @param {number} resultVersion Version of the LintResults object to return. + * @param {Object} fs File system implementation. * @param {boolean} synchronous Whether to execute synchronously. * @param {Function} callback Callback (err, result) function. * @returns {void} @@ -698,6 +698,7 @@ function lintFile( handleRuleFailures, noInlineConfig, resultVersion, + fs, synchronous, callback) { // eslint-disable-next-line jsdoc/require-jsdoc @@ -710,7 +711,6 @@ function lintFile( } // Make a/synchronous call to read file if (synchronous) { - // @ts-ignore lintContentWrapper(null, fs.readFileSync(file, "utf8")); } else { fs.readFile(file, "utf8", lintContentWrapper); @@ -756,6 +756,7 @@ function lintInput(options, synchronous, callback) { // @ts-ignore md.use(...plugin); }); + const fs = options.fs || require("fs"); const results = newResults(ruleList); let done = false; // Linting of strings is always synchronous @@ -795,6 +796,7 @@ function lintInput(options, synchronous, callback) { handleRuleFailures, noInlineConfig, resultVersion, + fs, synchronous, syncCallback ); @@ -819,6 +821,7 @@ function lintInput(options, synchronous, callback) { handleRuleFailures, noInlineConfig, resultVersion, + fs, synchronous, (err, result) => { concurrency--; @@ -929,17 +932,17 @@ function parseConfiguration(name, content, parsers) { * * @param {string} configFile Configuration file name. * @param {string} referenceId Referenced identifier to resolve. + * @param {Object} fs File system implementation. * @returns {string} Resolved path to file. */ -function resolveConfigExtends(configFile, referenceId) { +function resolveConfigExtends(configFile, referenceId, fs) { const configFileDirname = path.dirname(configFile); const resolvedExtendsFile = path.resolve(configFileDirname, referenceId); try { - if (fs.statSync(resolvedExtendsFile).isFile()) { - return resolvedExtendsFile; - } + fs.accessSync(resolvedExtendsFile); + return resolvedExtendsFile; } catch { - // If not a file or fs.statSync throws, try require.resolve + // Not a file, try require.resolve } try { return dynamicRequire.resolve( @@ -947,7 +950,7 @@ function resolveConfigExtends(configFile, referenceId) { { "paths": [ configFileDirname ] } ); } catch { - // If require.resolve throws, return resolvedExtendsFile + // Unable to resolve, return resolvedExtendsFile } return resolvedExtendsFile; } @@ -958,14 +961,23 @@ function resolveConfigExtends(configFile, referenceId) { * @param {string} file Configuration file name. * @param {ConfigurationParser[] | ReadConfigCallback} parsers Parsing * function(s). + * @param {Object} [fs] File system implementation. * @param {ReadConfigCallback} [callback] Callback (err, result) function. * @returns {void} */ -function readConfig(file, parsers, callback) { +function readConfig(file, parsers, fs, callback) { if (!callback) { - // @ts-ignore - callback = parsers; - parsers = null; + if (fs) { + callback = fs; + fs = null; + } else { + // @ts-ignore + callback = parsers; + parsers = null; + } + } + if (!fs) { + fs = require("fs"); } // Read file fs.readFile(file, "utf8", (err, content) => { @@ -982,8 +994,8 @@ function readConfig(file, parsers, callback) { const configExtends = config.extends; if (configExtends) { delete config.extends; - const resolvedExtends = resolveConfigExtends(file, configExtends); - return readConfig(resolvedExtends, parsers, (errr, extendsConfig) => { + const resolvedExtends = resolveConfigExtends(file, configExtends, fs); + return readConfig(resolvedExtends, parsers, fs, (errr, extendsConfig) => { if (errr) { return callback(errr); } @@ -1004,11 +1016,12 @@ const readConfigPromisify = promisify && promisify(readConfig); * * @param {string} file Configuration file name. * @param {ConfigurationParser[]} [parsers] Parsing function(s). + * @param {Object} [fs] File system implementation. * @returns {Promise} Configuration object. */ -function readConfigPromise(file, parsers) { +function readConfigPromise(file, parsers, fs) { // @ts-ignore - return readConfigPromisify(file, parsers); + return readConfigPromisify(file, parsers, fs); } /** @@ -1016,11 +1029,14 @@ function readConfigPromise(file, parsers) { * * @param {string} file Configuration file name. * @param {ConfigurationParser[]} [parsers] Parsing function(s). + * @param {Object} [fs] File system implementation. * @returns {Configuration} Configuration object. */ -function readConfigSync(file, parsers) { +function readConfigSync(file, parsers, fs) { + if (!fs) { + fs = require("fs"); + } // Read file - // @ts-ignore const content = fs.readFileSync(file, "utf8"); // Try to parse file const { config, message } = parseConfiguration(file, content, parsers); @@ -1031,9 +1047,9 @@ function readConfigSync(file, parsers) { const configExtends = config.extends; if (configExtends) { delete config.extends; - const resolvedExtends = resolveConfigExtends(file, configExtends); + const resolvedExtends = resolveConfigExtends(file, configExtends, fs); return { - ...readConfigSync(resolvedExtends, parsers), + ...readConfigSync(resolvedExtends, parsers, fs), ...config }; } @@ -1156,6 +1172,7 @@ module.exports = markdownlint; * @property {boolean} [noInlineConfig] True to ignore HTML directives. * @property {number} [resultVersion] Results object version. * @property {Plugin[]} [markdownItPlugins] Additional plugins. + * @property {Object} [fs] File system implementation. */ /** diff --git a/test/markdownlint-test.js b/test/markdownlint-test.js index 174e43f3..dbe1c4a8 100644 --- a/test/markdownlint-test.js +++ b/test/markdownlint-test.js @@ -652,11 +652,13 @@ test.cb("readmeHeadings", (t) => { "##### options.noInlineConfig", "##### options.resultVersion", "##### options.markdownItPlugins", + "##### options.fs", "#### callback", "#### result", "### Config", "#### file", "#### parsers", + "#### fs", "#### callback", "#### result", "## Usage", @@ -798,6 +800,40 @@ test.cb("missingStringValue", (t) => { }); }); +test("customFileSystemSync", (t) => { + t.plan(2); + const file = "/dir/file.md"; + const fsApi = { + "readFileSync": (p) => { + t.is(p, file); + return "# Heading"; + } + }; + const result = markdownlint.sync({ + "files": file, + "fs": fsApi + }); + t.deepEqual(result[file].length, 1, "Did not report violations."); +}); + +test.cb("customFileSystemAsync", (t) => { + t.plan(3); + const file = "/dir/file.md"; + const fsApi = { + "readFile": (p, o, cb) => { + t.is(p, file); + cb(null, "# Heading"); + } + }; + markdownlint({ + "files": file, + "fs": fsApi + }, function callback(err, result) { + t.falsy(err); + t.deepEqual(result[file].length, 1, "Did not report violations."); + t.end(); + }); +}); test.cb("readme", (t) => { t.plan(115); const tagToRules = {}; @@ -1105,6 +1141,52 @@ test.cb("configMultipleWithRequireResolve", (t) => { }); }); +test.cb("configCustomFileSystem", (t) => { + t.plan(5); + const file = "/dir/file.json"; + const extended = "/dir/extended.json"; + const fileContent = { + "extends": extended, + "default": true, + "MD001": false + }; + const extendedContent = { + "MD001": true, + "MD002": true + }; + const fsApi = { + "accessSync": (p) => { + t.is(p, extended); + }, + "readFile": (p, o, cb) => { + switch (p) { + case file: + t.is(p, file); + return cb(null, JSON.stringify(fileContent)); + case extended: + t.is(p, extended); + return cb(null, JSON.stringify(extendedContent)); + default: + return t.fail(); + } + } + }; + markdownlint.readConfig( + file, + null, + fsApi, + function callback(err, actual) { + t.falsy(err); + const expected = { + ...extendedContent, + ...fileContent + }; + delete expected.extends; + t.deepEqual(actual, expected, "Config object not correct."); + t.end(); + }); +}); + test.cb("configBadFile", (t) => { t.plan(4); markdownlint.readConfig("./test/config/config-badfile.json", @@ -1358,6 +1440,45 @@ test("configMultipleHybridSync", (t) => { t.like(actual, expected, "Config object not correct."); }); +test("configCustomFileSystemSync", (t) => { + t.plan(4); + const file = "/dir/file.json"; + const extended = "/dir/extended.json"; + const fileContent = { + "extends": extended, + "default": true, + "MD001": false + }; + const extendedContent = { + "MD001": true, + "MD002": true + }; + const fsApi = { + "accessSync": (p) => { + t.is(p, extended); + }, + "readFileSync": (p) => { + switch (p) { + case file: + t.is(p, file); + return JSON.stringify(fileContent); + case extended: + t.is(p, extended); + return JSON.stringify(extendedContent); + default: + return t.fail(); + } + } + }; + const actual = markdownlint.readConfigSync(file, null, fsApi); + const expected = { + ...extendedContent, + ...fileContent + }; + delete expected.extends; + t.deepEqual(actual, expected, "Config object not correct."); +}); + test("configBadHybridSync", (t) => { t.plan(1); t.throws( @@ -1385,6 +1506,48 @@ test.cb("configSinglePromise", (t) => { }); }); +test.cb("configCustomFileSystemPromise", (t) => { + t.plan(4); + const file = "/dir/file.json"; + const extended = "/dir/extended.json"; + const fileContent = { + "extends": extended, + "default": true, + "MD001": false + }; + const extendedContent = { + "MD001": true, + "MD002": true + }; + const fsApi = { + "accessSync": (p) => { + t.is(p, extended); + }, + "readFile": (p, o, cb) => { + switch (p) { + case file: + t.is(p, file); + return cb(null, JSON.stringify(fileContent)); + case extended: + t.is(p, extended); + return cb(null, JSON.stringify(extendedContent)); + default: + return t.fail(); + } + } + }; + markdownlint.promises.readConfig(file, null, fsApi) + .then((actual) => { + const expected = { + ...extendedContent, + ...fileContent + }; + delete expected.extends; + t.deepEqual(actual, expected, "Config object not correct."); + t.end(); + }); +}); + test.cb("configBadFilePromise", (t) => { t.plan(2); markdownlint.promises.readConfig("./test/config/config-badfile.json")