Support tilde paths ("~" prefix) in readConfig/Sync APIs (for "file" parameter and "extends" keys).

This commit is contained in:
David Anson 2022-05-16 22:57:11 -07:00
parent 5505deb1c9
commit ffc4d56918
6 changed files with 132 additions and 31 deletions

View file

@ -639,7 +639,8 @@ The `file` is resolved relative to the current working directory. If an `extends
key is present once read, its value will be resolved as a path relative to `file` key is present once read, its value will be resolved as a path relative to `file`
and loaded recursively. Settings from a file referenced by `extends` are applied and loaded recursively. Settings from a file referenced by `extends` are applied
first, then those of `file` are applied on top (overriding any of the same keys first, then those of `file` are applied on top (overriding any of the same keys
appearing in the referenced file). appearing in the referenced file). If either the `file` or `extends` path begins
with the `~` directory, it will act as a placeholder for the home directory.
#### parsers #### parsers

View file

@ -995,6 +995,18 @@ function deepFreeze(obj) {
return obj; return obj;
} }
module.exports.deepFreeze = deepFreeze; module.exports.deepFreeze = deepFreeze;
/**
* Expands a path with a tilde to an absolute path.
*
* @param {string} file Path that may begin with a tilde.
* @param {Object} os Node.js "os" module.
* @returns {string} Absolute path (or original path).
*/
function expandTildePath(file, os) {
var homedir = os && os.homedir();
return homedir ? file.replace(/^~($|\/|\\)/, "".concat(homedir, "$1")) : file;
}
module.exports.expandTildePath = expandTildePath;
/***/ }), /***/ }),
@ -1030,6 +1042,16 @@ module.exports = markdownit;
/***/ }), /***/ }),
/***/ "?a32b":
/*!********************!*\
!*** os (ignored) ***!
\********************/
/***/ (() => {
/* (ignored) */
/***/ }),
/***/ "?b85c": /***/ "?b85c":
/*!**********************!*\ /*!**********************!*\
!*** path (ignored) ***! !*** path (ignored) ***!
@ -2070,6 +2092,8 @@ function readConfig(file, parsers, fs, callback) {
fs = __webpack_require__(/*! fs */ "?ec0a"); fs = __webpack_require__(/*! fs */ "?ec0a");
} }
// Read file // Read file
var os = __webpack_require__(/*! os */ "?a32b");
file = helpers.expandTildePath(file, os);
fs.readFile(file, "utf8", function (err, content) { fs.readFile(file, "utf8", function (err, content) {
if (err) { if (err) {
return callback(err); return callback(err);
@ -2084,7 +2108,7 @@ function readConfig(file, parsers, fs, callback) {
var configExtends = config.extends; var configExtends = config.extends;
if (configExtends) { if (configExtends) {
delete config.extends; delete config.extends;
return resolveConfigExtends(file, configExtends, fs, function (_, resolvedExtends) { return readConfig(resolvedExtends, parsers, fs, function (errr, extendsConfig) { return resolveConfigExtends(file, helpers.expandTildePath(configExtends, os), fs, function (_, resolvedExtends) { return readConfig(resolvedExtends, parsers, fs, function (errr, extendsConfig) {
if (errr) { if (errr) {
return callback(errr); return callback(errr);
} }
@ -2121,6 +2145,8 @@ function readConfigSync(file, parsers, fs) {
fs = __webpack_require__(/*! fs */ "?ec0a"); fs = __webpack_require__(/*! fs */ "?ec0a");
} }
// Read file // Read file
var os = __webpack_require__(/*! os */ "?a32b");
file = helpers.expandTildePath(file, os);
var content = fs.readFileSync(file, "utf8"); var content = fs.readFileSync(file, "utf8");
// Try to parse file // Try to parse file
var _a = parseConfiguration(file, content, parsers), config = _a.config, message = _a.message; var _a = parseConfiguration(file, content, parsers), config = _a.config, message = _a.message;
@ -2131,7 +2157,7 @@ function readConfigSync(file, parsers, fs) {
var configExtends = config.extends; var configExtends = config.extends;
if (configExtends) { if (configExtends) {
delete config.extends; delete config.extends;
var resolvedExtends = resolveConfigExtendsSync(file, configExtends, fs); var resolvedExtends = resolveConfigExtendsSync(file, helpers.expandTildePath(configExtends, os), fs);
return __assign(__assign({}, readConfigSync(resolvedExtends, parsers, fs)), config); return __assign(__assign({}, readConfigSync(resolvedExtends, parsers, fs)), config);
} }
return config; return config;

View file

@ -1021,3 +1021,16 @@ function deepFreeze(obj) {
return obj; return obj;
} }
module.exports.deepFreeze = deepFreeze; module.exports.deepFreeze = deepFreeze;
/**
* Expands a path with a tilde to an absolute path.
*
* @param {string} file Path that may begin with a tilde.
* @param {Object} os Node.js "os" module.
* @returns {string} Absolute path (or original path).
*/
function expandTildePath(file, os) {
const homedir = os && os.homedir();
return homedir ? file.replace(/^~($|\/|\\)/, `${homedir}$1`) : file;
}
module.exports.expandTildePath = expandTildePath;

View file

@ -1026,6 +1026,8 @@ function readConfig(file, parsers, fs, callback) {
fs = require("fs"); fs = require("fs");
} }
// Read file // Read file
const os = require("os");
file = helpers.expandTildePath(file, os);
fs.readFile(file, "utf8", (err, content) => { fs.readFile(file, "utf8", (err, content) => {
if (err) { if (err) {
return callback(err); return callback(err);
@ -1042,7 +1044,7 @@ function readConfig(file, parsers, fs, callback) {
delete config.extends; delete config.extends;
return resolveConfigExtends( return resolveConfigExtends(
file, file,
configExtends, helpers.expandTildePath(configExtends, os),
fs, fs,
(_, resolvedExtends) => readConfig( (_, resolvedExtends) => readConfig(
resolvedExtends, resolvedExtends,
@ -1093,6 +1095,8 @@ function readConfigSync(file, parsers, fs) {
fs = require("fs"); fs = require("fs");
} }
// Read file // Read file
const os = require("os");
file = helpers.expandTildePath(file, os);
const content = fs.readFileSync(file, "utf8"); const content = fs.readFileSync(file, "utf8");
// Try to parse file // Try to parse file
const { config, message } = parseConfiguration(file, content, parsers); const { config, message } = parseConfiguration(file, content, parsers);
@ -1103,7 +1107,11 @@ function readConfigSync(file, parsers, fs) {
const configExtends = config.extends; const configExtends = config.extends;
if (configExtends) { if (configExtends) {
delete config.extends; delete config.extends;
const resolvedExtends = resolveConfigExtendsSync(file, configExtends, fs); const resolvedExtends = resolveConfigExtendsSync(
file,
helpers.expandTildePath(configExtends, os),
fs
);
return { return {
...readConfigSync(resolvedExtends, parsers, fs), ...readConfigSync(resolvedExtends, parsers, fs),
...config ...config

View file

@ -2,10 +2,13 @@
"use strict"; "use strict";
const os = require("os");
const path = require("path"); const path = require("path");
const test = require("ava").default; const test = require("ava").default;
const markdownlint = require("../lib/markdownlint"); const markdownlint = require("../lib/markdownlint");
const sameFileSystem = (path.relative(os.homedir(), __dirname) !== __dirname);
test.cb("configSingle", (t) => { test.cb("configSingle", (t) => {
t.plan(2); t.plan(2);
markdownlint.readConfig("./test/config/config-child.json", markdownlint.readConfig("./test/config/config-child.json",
@ -28,6 +31,20 @@ test.cb("configAbsolute", (t) => {
}); });
}); });
if (sameFileSystem) {
test.cb("configTilde", (t) => {
t.plan(2);
markdownlint.readConfig(
`~/${path.relative(os.homedir(), "./test/config/config-child.json")}`,
function callback(err, actual) {
t.falsy(err);
const expected = require("./config/config-child.json");
t.deepEqual(actual, expected, "Config object not correct.");
t.end();
});
});
}
test.cb("configMultiple", (t) => { test.cb("configMultiple", (t) => {
t.plan(2); t.plan(2);
markdownlint.readConfig("./test/config/config-grandparent.json", markdownlint.readConfig("./test/config/config-grandparent.json",
@ -60,9 +77,10 @@ test.cb("configMultipleWithRequireResolve", (t) => {
}); });
test.cb("configCustomFileSystem", (t) => { test.cb("configCustomFileSystem", (t) => {
t.plan(5); t.plan(3);
const file = path.resolve("/dir/file.json"); const file = "/dir/file.json";
const extended = path.resolve("/dir/extended.json"); const extended = "~/dir/extended.json";
const expanded = path.join(os.homedir(), extended.slice(1));
const fileContent = { const fileContent = {
"extends": extended, "extends": extended,
"default": true, "default": true,
@ -74,20 +92,16 @@ test.cb("configCustomFileSystem", (t) => {
}; };
const fsApi = { const fsApi = {
"access": (p, m, cb) => { "access": (p, m, cb) => {
t.is(p, extended); t.is(p, expanded);
return (cb || m)(); return (cb || m)();
}, },
"readFile": (p, o, cb) => { "readFile": (p, o, cb) => {
switch (p) { if (p === file) {
case file:
t.is(p, file);
return cb(null, JSON.stringify(fileContent)); return cb(null, JSON.stringify(fileContent));
case extended: } else if (p === expanded) {
t.is(p, extended);
return cb(null, JSON.stringify(extendedContent)); return cb(null, JSON.stringify(extendedContent));
default:
return t.fail();
} }
return t.fail(p);
} }
}; };
markdownlint.readConfig( markdownlint.readConfig(
@ -254,6 +268,16 @@ test("configAbsoluteSync", (t) => {
t.deepEqual(actual, expected, "Config object not correct."); t.deepEqual(actual, expected, "Config object not correct.");
}); });
if (sameFileSystem) {
test("configTildeSync", (t) => {
t.plan(1);
const actual = markdownlint.readConfigSync(
`~/${path.relative(os.homedir(), "./test/config/config-child.json")}`);
const expected = require("./config/config-child.json");
t.deepEqual(actual, expected, "Config object not correct.");
});
}
test("configMultipleSync", (t) => { test("configMultipleSync", (t) => {
t.plan(1); t.plan(1);
const actual = const actual =
@ -362,9 +386,10 @@ test("configMultipleHybridSync", (t) => {
}); });
test("configCustomFileSystemSync", (t) => { test("configCustomFileSystemSync", (t) => {
t.plan(4); t.plan(2);
const file = path.resolve("/dir/file.json"); const file = "/dir/file.json";
const extended = path.resolve("/dir/extended.json"); const extended = "~/dir/extended.json";
const expanded = path.join(os.homedir(), extended.slice(1));
const fileContent = { const fileContent = {
"extends": extended, "extends": extended,
"default": true, "default": true,
@ -376,19 +401,15 @@ test("configCustomFileSystemSync", (t) => {
}; };
const fsApi = { const fsApi = {
"accessSync": (p) => { "accessSync": (p) => {
t.is(p, extended); t.is(p, expanded);
}, },
"readFileSync": (p) => { "readFileSync": (p) => {
switch (p) { if (p === file) {
case file:
t.is(p, file);
return JSON.stringify(fileContent); return JSON.stringify(fileContent);
case extended: } else if (p === expanded) {
t.is(p, extended);
return JSON.stringify(extendedContent); return JSON.stringify(extendedContent);
default:
return t.fail();
} }
return t.fail(p);
} }
}; };
const actual = markdownlint.readConfigSync(file, null, fsApi); const actual = markdownlint.readConfigSync(file, null, fsApi);

View file

@ -2,6 +2,8 @@
"use strict"; "use strict";
const os = require("os");
const path = require("path");
const test = require("ava").default; const test = require("ava").default;
const helpers = require("../helpers"); const helpers = require("../helpers");
@ -1303,3 +1305,33 @@ test("htmlElementRanges", (t) => {
const actual = helpers.htmlElementRanges(params, lineMetadata); const actual = helpers.htmlElementRanges(params, lineMetadata);
t.deepEqual(actual, expected); t.deepEqual(actual, expected);
}); });
test("expandTildePath", (t) => {
t.plan(16);
const homedir = os.homedir();
t.is(helpers.expandTildePath("", os), "");
t.is(helpers.expandTildePath("", null), "");
t.is(
path.resolve(helpers.expandTildePath("~", os)),
homedir
);
t.is(helpers.expandTildePath("~", null), "~");
t.is(helpers.expandTildePath("file", os), "file");
t.is(helpers.expandTildePath("file", null), "file");
t.is(helpers.expandTildePath("/file", os), "/file");
t.is(helpers.expandTildePath("/file", null), "/file");
t.is(
path.resolve(helpers.expandTildePath("~/file", os)),
path.join(homedir, "/file")
);
t.is(helpers.expandTildePath("~/file", null), "~/file");
t.is(helpers.expandTildePath("dir/file", os), "dir/file");
t.is(helpers.expandTildePath("dir/file", null), "dir/file");
t.is(helpers.expandTildePath("/dir/file", os), "/dir/file");
t.is(helpers.expandTildePath("/dir/file", null), "/dir/file");
t.is(
path.resolve(helpers.expandTildePath("~/dir/file", os)),
path.join(homedir, "/dir/file")
);
t.is(helpers.expandTildePath("~/dir/file", null), "~/dir/file");
});