diff --git a/markdownlint-cli2.mjs b/markdownlint-cli2.mjs index 46c3cb72..dd02ca24 100755 --- a/markdownlint-cli2.mjs +++ b/markdownlint-cli2.mjs @@ -15,7 +15,7 @@ import { lint, extendConfig, readConfig } from "markdownlint/promise"; import { expandTildePath } from "markdownlint/helpers"; import appendToArray from "./append-to-array.mjs"; import mergeOptions from "./merge-options.mjs"; -import resolveAndRequire from "./resolve-and-require.mjs"; +import resolveModule from "./resolve-module.mjs"; import parsers from "./parsers/parsers.mjs"; import jsoncParse from "./parsers/jsonc-parse.mjs"; import yamlParse from "./parsers/yaml-parse.mjs"; @@ -65,73 +65,62 @@ const readConfigFile = (fs, dir, name, otherwise) => () => { ); }; -// Import or resolve/require a module ID with a custom directory in the path -const importOrRequireResolve = async (dirOrDirs, id, noRequire) => { - if (typeof id === "string") { - if (noRequire) { - return null; - } - const dirs = Array.isArray(dirOrDirs) ? dirOrDirs : [ dirOrDirs ]; - const expandId = expandTildePath(id, os); - const errors = []; - // Try to load via require(...) - try { - const isModule = /\.mjs$/iu.test(expandId); - if (!isModule) { - // Try not to use require for modules due to breaking change in Node 22.12: - // https://github.com/nodejs/node/releases/tag/v22.12.0 - return resolveAndRequire(dynamicRequire, expandId, dirs); - } - } catch (error) { - errors.push(error); - } - // Try to load via import(...) +// Import a module ID with a custom directory in the path +const importModule = async (dirOrDirs, id, noRequire) => { + if (typeof id !== "string") { + return id; + } else if (noRequire) { + return null; + } + const expandId = expandTildePath(id, os); + const dirs = Array.isArray(dirOrDirs) ? dirOrDirs : [ dirOrDirs ]; + try { + let moduleName = null; try { - // eslint-disable-next-line n/no-unsupported-features/node-builtins - const isURL = !pathDefault.isAbsolute(expandId) && URL.canParse(expandId); - const urlString = ( - isURL ? new URL(expandId) : pathToFileURL(pathDefault.resolve(dirs[0], expandId)) - ).toString(); - // eslint-disable-next-line no-inline-comments - const module = await import(/* webpackIgnore: true */ urlString); - return module.default; - } catch (error) { - errors.push(error); + moduleName = resolveModule(dynamicRequire, expandId, dirs); + } catch { + moduleName = + // eslint-disable-next-line n/no-unsupported-features/node-builtins + (!pathDefault.isAbsolute(expandId) && URL.canParse(expandId)) + ? new URL(expandId) + : pathToFileURL(pathDefault.resolve(dirs[0], expandId)); } - // Give up - throw new AggregateError( - errors, - `Unable to require or import module '${id}'.` + // eslint-disable-next-line no-inline-comments + const module = await import(/* webpackIgnore: true */ moduleName); + return module.default; + } catch (error) { + throw new Error( + `Unable to require or import module '${id}'.`, + { "cause": error } ); } - return id; }; -// Import or require an array of modules by ID -const importOrRequireIds = (dirs, ids, noRequire) => ( +// Import an array of modules by ID +const importModuleIds = (dirs, ids, noRequire) => ( Promise.all( ids.map( - (id) => importOrRequireResolve(dirs, id, noRequire) + (id) => importModule(dirs, id, noRequire) ) ).then((results) => results.filter(Boolean)) ); -// Import or require an array of modules by ID (preserving parameters) -const importOrRequireIdsAndParams = (dirs, idsAndParams, noRequire) => ( +// Import an array of modules by ID (preserving parameters) +const importModuleIdsAndParams = (dirs, idsAndParams, noRequire) => ( Promise.all( idsAndParams.map( - (idAndParams) => importOrRequireResolve(dirs, idAndParams[0], noRequire). + (idAndParams) => importModule(dirs, idAndParams[0], noRequire). then((module) => module && [ module, ...idAndParams.slice(1) ]) ) ).then((results) => results.filter(Boolean)) ); -// Import or require a JavaScript file and return the exported object -const importOrRequireConfig = (fs, dir, name, noRequire, otherwise) => () => { +// Import a JavaScript file and return the exported object +const importConfig = (fs, dir, name, noRequire, otherwise) => () => { const file = pathPosix.join(dir, name); return fs.promises.access(file). then( - () => importOrRequireResolve(dir, name, noRequire), + () => importModule(dir, name, noRequire), otherwise ); }; @@ -165,7 +154,7 @@ const readOptionsOrConfig = async (configPath, fs, noRequire) => { basename.endsWith(".markdownlint-cli2.cjs") || basename.endsWith(".markdownlint-cli2.mjs") ) { - options = await importOrRequireResolve(dirname, basename, noRequire); + options = await importModule(dirname, basename, noRequire); } else if ( basename.endsWith(".markdownlint.jsonc") || basename.endsWith(".markdownlint.json") || @@ -177,7 +166,7 @@ const readOptionsOrConfig = async (configPath, fs, noRequire) => { basename.endsWith(".markdownlint.cjs") || basename.endsWith(".markdownlint.mjs") ) { - config = await importOrRequireResolve(dirname, basename, noRequire); + config = await importModule(dirname, basename, noRequire); } else { throw new Error( "File name should be (or end with) one of the supported types " + @@ -322,10 +311,10 @@ const getAndProcessDirInfo = ( () => fs.promises.readFile(file, utf8).then(yamlParse), () => fs.promises.access(captureFile(markdownlintCli2Cjs)). then( - () => importOrRequireResolve(dir, file, noRequire), + () => importModule(dir, file, noRequire), () => fs.promises.access(captureFile(markdownlintCli2Mjs)). then( - () => importOrRequireResolve(dir, file, noRequire), + () => importModule(dir, file, noRequire), () => (allowPackageJson ? fs.promises.access(captureFile(packageJson)) // eslint-disable-next-line prefer-promise-reject-errors @@ -379,12 +368,12 @@ const getAndProcessDirInfo = ( fs, dir, ".markdownlint.yml", - importOrRequireConfig( + importConfig( fs, dir, ".markdownlint.cjs", noRequire, - importOrRequireConfig( + importConfig( fs, dir, ".markdownlint.mjs", @@ -650,7 +639,7 @@ const createDirInfos = async ( ); if (markdownlintOptions && markdownlintOptions.customRules) { tasks.push( - importOrRequireIds( + importModuleIds( [ effectiveDir, ...effectiveModulePaths ], markdownlintOptions.customRules, noRequire @@ -662,7 +651,7 @@ const createDirInfos = async ( } if (markdownlintOptions && markdownlintOptions.markdownItPlugins) { tasks.push( - importOrRequireIdsAndParams( + importModuleIdsAndParams( [ effectiveDir, ...effectiveModulePaths ], markdownlintOptions.markdownItPlugins, noRequire @@ -871,7 +860,7 @@ const outputSummary = async ( const dir = relativeDir || baseDir; const dirs = [ dir, ...modulePaths ]; const formattersAndParams = outputFormatters - ? await importOrRequireIdsAndParams(dirs, outputFormatters, noRequire) + ? await importModuleIdsAndParams(dirs, outputFormatters, noRequire) // eslint-disable-next-line no-inline-comments, unicorn/no-await-expression-member : [ [ (await import(/* webpackMode: "eager" */ "markdownlint-cli2-formatter-default")).default ] ]; await Promise.all(formattersAndParams.map((formatterAndParams) => { diff --git a/package.json b/package.json index fee89bcf..7d53a6e6 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "playwright-test": "playwright test --config ./webworker/playwright.config.mjs", "playwright-test-docker": "docker run --rm --volume $PWD:/home/workdir --workdir /home/workdir --ipc=host mcr.microsoft.com/playwright:v1.49.1 npm run playwright-test", "schema": "cpy ./node_modules/markdownlint/schema/markdownlint-config-schema.json ./schema --flat", - "test": "ava --timeout=1m test/append-to-array-test.mjs test/fs-mock-test.mjs test/fs-virtual-test.mjs test/markdownlint-cli2-test.mjs test/markdownlint-cli2-test-exec.mjs test/markdownlint-cli2-test-exports.mjs test/markdownlint-cli2-test-fs.mjs test/markdownlint-cli2-test-main.mjs test/merge-options-test.mjs test/resolve-and-require-test.mjs", + "test": "ava --timeout=1m test/append-to-array-test.mjs test/fs-mock-test.mjs test/fs-virtual-test.mjs test/markdownlint-cli2-test.mjs test/markdownlint-cli2-test-exec.mjs test/markdownlint-cli2-test-exports.mjs test/markdownlint-cli2-test-fs.mjs test/markdownlint-cli2-test-main.mjs test/merge-options-test.mjs test/resolve-module-test.mjs", "test-cover": "c8 --100 npm test", "test-docker-hub-image": "VERSION=$(node -e \"process.stdout.write(require('./package.json').version)\") && docker image rm davidanson/markdownlint-cli2:v$VERSION davidanson/markdownlint-cli2:latest || true && docker run --rm -v $PWD:/workdir davidanson/markdownlint-cli2:v$VERSION \"*.md\" && docker run --rm -v $PWD:/workdir davidanson/markdownlint-cli2:latest \"*.md\"", "test-docker-hub-image-rules": "VERSION=$(node -e \"process.stdout.write(require('./package.json').version)\") && docker image rm davidanson/markdownlint-cli2-rules:v$VERSION davidanson/markdownlint-cli2-rules:latest || true && docker run --rm -v $PWD:/workdir davidanson/markdownlint-cli2-rules:v$VERSION \"*.md\" && docker run --rm -v $PWD:/workdir davidanson/markdownlint-cli2-rules:latest \"*.md\"", @@ -68,7 +68,7 @@ "parsers/jsonc-parse.mjs", "parsers/yaml-parse.mjs", "README.md", - "resolve-and-require.mjs", + "resolve-module.mjs", "schema/markdownlint-cli2-config-schema.json", "schema/markdownlint-config-schema.json", "schema/ValidatingConfiguration.md" diff --git a/resolve-and-require.mjs b/resolve-and-require.mjs deleted file mode 100644 index 8650ee2e..00000000 --- a/resolve-and-require.mjs +++ /dev/null @@ -1,17 +0,0 @@ -// @ts-check - -/** - * Wrapper for calling Node's require.resolve/require with an additional path. - * @param {object} req Node's require implementation (or equivalent). - * @param {string} id Package identifier to require. - * @param {string[]} dirs Directories to include when resolving paths. - * @returns {object} Exported module content. - */ -const resolveAndRequire = (req, id, dirs) => { - const resolvePaths = req.resolve.paths ? req.resolve.paths("") : []; - const paths = [ ...dirs, ...resolvePaths ]; - const resolved = req.resolve(id, { paths }); - return req(resolved); -}; - -export default resolveAndRequire; diff --git a/resolve-module.mjs b/resolve-module.mjs new file mode 100644 index 00000000..13c87ac5 --- /dev/null +++ b/resolve-module.mjs @@ -0,0 +1,16 @@ +// @ts-check + +/** + * Wrapper for calling Node's require.resolve with additional paths. + * @param {object} require Node's require implementation (or equivalent). + * @param {string} request Module path to require. + * @param {string[]} paths Paths to resolve module location from. + * @returns {string} Resolved file name. + */ +const resolveModule = (require, request, paths) => { + const resolvePaths = require.resolve.paths ? require.resolve.paths("") : []; + const allPaths = [ ...paths, ...resolvePaths ]; + return require.resolve(request, { "paths": allPaths }); +}; + +export default resolveModule; diff --git a/test/customRules/dir/subdir/.markdownlint-cli2.jsonc b/test/customRules/dir/subdir/.markdownlint-cli2.jsonc index 66c3cb4b..5bb6868d 100644 --- a/test/customRules/dir/subdir/.markdownlint-cli2.jsonc +++ b/test/customRules/dir/subdir/.markdownlint-cli2.jsonc @@ -1,7 +1,7 @@ { "customRules": [ "../../rules/first-line.cjs", - "../../node_modules/markdownlint-rule-sample-commonjs", + "../../node_modules/markdownlint-rule-sample-commonjs/sample-rule.cjs", "../../node_modules/markdownlint-rule-sample-module/sample-rule.mjs", "markdownlint-rule-extended-ascii" ] diff --git a/test/customRules/dir2/.markdownlint-cli2.jsonc b/test/customRules/dir2/.markdownlint-cli2.jsonc index 8a668d71..9c7b3fec 100644 --- a/test/customRules/dir2/.markdownlint-cli2.jsonc +++ b/test/customRules/dir2/.markdownlint-cli2.jsonc @@ -1,6 +1,6 @@ { "customRules": [ "markdownlint-rule-sample-commonjs", - "../node_modules/markdownlint-rule-sample-module/sample-rule.mjs" + "markdownlint-rule-sample-module" ] } diff --git a/test/customRules/dir3/.markdownlint-cli2.jsonc b/test/customRules/dir3/.markdownlint-cli2.jsonc index e9a71c6c..a931ee9b 100644 --- a/test/customRules/dir3/.markdownlint-cli2.jsonc +++ b/test/customRules/dir3/.markdownlint-cli2.jsonc @@ -2,6 +2,6 @@ "customRules": [ "../rules/all-rules.cjs", "../node_modules/markdownlint-rule-sample-commonjs", - "../node_modules/markdownlint-rule-sample-module/sample-rule.mjs" + "../node_modules/markdownlint-rule-sample-module" ] } diff --git a/test/customRules/dir4/.markdownlint-cli2.mjs b/test/customRules/dir4/.markdownlint-cli2.mjs index f3d232e0..06e30e27 100644 --- a/test/customRules/dir4/.markdownlint-cli2.mjs +++ b/test/customRules/dir4/.markdownlint-cli2.mjs @@ -2,6 +2,10 @@ const options = { "customRules": [ + (new URL( + "../node_modules/markdownlint-rule-sample-commonjs/sample-rule.cjs", + import.meta.url + )).toString(), (new URL( "../node_modules/markdownlint-rule-sample-module/sample-rule.mjs", import.meta.url diff --git a/test/outputFormatters-module/.markdownlint-cli2.jsonc b/test/outputFormatters-module/.markdownlint-cli2.jsonc index f64f929a..3769a370 100644 --- a/test/outputFormatters-module/.markdownlint-cli2.jsonc +++ b/test/outputFormatters-module/.markdownlint-cli2.jsonc @@ -1,6 +1,8 @@ { "outputFormatters": [ [ "output-formatter-sample-commonjs" ], + [ "output-formatter-sample-module" ], + [ "./node_modules/output-formatter-sample-commonjs/sample-formatter.cjs" ], [ "./node_modules/output-formatter-sample-module/sample-formatter.mjs" ] ] } diff --git a/test/resolve-and-require-test.mjs b/test/resolve-module-test.mjs similarity index 63% rename from test/resolve-and-require-test.mjs rename to test/resolve-module-test.mjs index 54049592..2db0f549 100644 --- a/test/resolve-and-require-test.mjs +++ b/test/resolve-module-test.mjs @@ -3,7 +3,7 @@ import test from "ava"; import path from "node:path"; import { __dirname } from "./esm-helpers.mjs"; -import resolveAndRequire from "../resolve-and-require.mjs"; +import resolve from "../resolve-module.mjs"; import { createRequire } from "node:module"; const require = createRequire(import.meta.url); @@ -11,24 +11,24 @@ const require = createRequire(import.meta.url); test("built-in module", (t) => { t.plan(1); t.deepEqual( - require("node:fs"), - resolveAndRequire(require, "fs", [ __dirname(import.meta) ]) + require.resolve("node:fs"), + resolve(require, "node:fs", [ __dirname(import.meta) ]) ); }); test("locally-installed module", (t) => { t.plan(1); t.deepEqual( - require("micromatch"), - resolveAndRequire(require, "micromatch", [ __dirname(import.meta) ]) + require.resolve("micromatch"), + resolve(require, "micromatch", [ __dirname(import.meta) ]) ); }); test("relative (to __dirname(import.meta)) path to module", (t) => { t.plan(1); t.deepEqual( - require("./customRules/node_modules/markdownlint-rule-sample-commonjs"), - resolveAndRequire( + require.resolve("./customRules/node_modules/markdownlint-rule-sample-commonjs"), + resolve( require, "./customRules/node_modules/markdownlint-rule-sample-commonjs", [ __dirname(import.meta) ] @@ -40,12 +40,12 @@ test("module in alternate node_modules", (t) => { t.plan(2); t.throws( // @ts-ignore - () => require("markdownlint-rule-sample-commonjs"), + () => require.resolve("markdownlint-rule-sample-commonjs"), { "code": "MODULE_NOT_FOUND" } ); t.deepEqual( - require("./customRules/node_modules/markdownlint-rule-sample-commonjs"), - resolveAndRequire( + require.resolve("./customRules/node_modules/markdownlint-rule-sample-commonjs"), + resolve( require, "markdownlint-rule-sample-commonjs", [ path.join(__dirname(import.meta), "customRules") ] @@ -59,12 +59,12 @@ test("module in alternate node_modules and no require.resolve.paths", (t) => { delete require.resolve.paths; t.throws( // @ts-ignore - () => require("markdownlint-rule-sample-commonjs"), + () => require.resolve("markdownlint-rule-sample-commonjs"), { "code": "MODULE_NOT_FOUND" } ); t.deepEqual( - require("./customRules/node_modules/markdownlint-rule-sample-commonjs"), - resolveAndRequire( + require.resolve("./customRules/node_modules/markdownlint-rule-sample-commonjs"), + resolve( require, "markdownlint-rule-sample-commonjs", [ path.join(__dirname(import.meta), "customRules") ] @@ -79,20 +79,20 @@ test("module local, relative, and in alternate node_modules", (t) => { path.join(__dirname(import.meta), "customRules") ]; t.deepEqual( - require("micromatch"), - resolveAndRequire(require, "micromatch", dirs) + require.resolve("micromatch"), + resolve(require, "micromatch", dirs) ); t.deepEqual( - require("./customRules/node_modules/markdownlint-rule-sample-commonjs"), - resolveAndRequire( + require.resolve("./customRules/node_modules/markdownlint-rule-sample-commonjs"), + resolve( require, "./customRules/node_modules/markdownlint-rule-sample-commonjs", dirs ) ); t.deepEqual( - require("./customRules/node_modules/markdownlint-rule-sample-commonjs"), - resolveAndRequire( + require.resolve("./customRules/node_modules/markdownlint-rule-sample-commonjs"), + resolve( require, "markdownlint-rule-sample-commonjs", dirs diff --git a/test/snapshots/markdownlint-cli2-test-exec.mjs.md b/test/snapshots/markdownlint-cli2-test-exec.mjs.md index 8df1378b..314fb669 100644 --- a/test/snapshots/markdownlint-cli2-test-exec.mjs.md +++ b/test/snapshots/markdownlint-cli2-test-exec.mjs.md @@ -2726,6 +2726,7 @@ Generated by [AVA](https://avajs.dev). dir3/hr.md:2 second-line Rule that reports an error for the second line␊ dir3/hr.md:3 sample-rule-commonjs Sample rule (commonjs) [Sample error for hr]␊ dir3/hr.md:3 sample-rule-module Sample rule (module) [Sample error for hr]␊ + dir4/hr.md:3 sample-rule-commonjs Sample rule (commonjs) [Sample error for hr]␊ dir4/hr.md:3 sample-rule-module Sample rule (module) [Sample error for hr]␊ viewme.md:1 every-n-lines Rule that reports an error every N lines [This rule threw an exception: getLineMetadata is not a function]␊ viewme.md:1 first-line Rule that reports an error for the first line␊ @@ -2738,7 +2739,7 @@ Generated by [AVA](https://avajs.dev). stdout: `markdownlint-cli2 vX.Y.Z (markdownlint vX.Y.Z)␊ Finding: **/*.md␊ Linting: 9 file(s)␊ - Summary: 44 error(s)`, + Summary: 45 error(s)`, } ## customRules-pre-imported (exec) @@ -5705,6 +5706,16 @@ Generated by [AVA](https://avajs.dev). mjs: viewme.md 5 MD012/no-multiple-blanks␊ mjs: viewme.md 6 MD025/single-title/single-h1␊ mjs: viewme.md 12 MD019/no-multiple-space-atx␊ + mjs: viewme.md 14 MD047/single-trailing-newline␊ + cjs: viewme.md 3 MD009/no-trailing-spaces␊ + cjs: viewme.md 5 MD012/no-multiple-blanks␊ + cjs: viewme.md 6 MD025/single-title/single-h1␊ + cjs: viewme.md 12 MD019/no-multiple-space-atx␊ + cjs: viewme.md 14 MD047/single-trailing-newline␊ + mjs: viewme.md 3 MD009/no-trailing-spaces␊ + mjs: viewme.md 5 MD012/no-multiple-blanks␊ + mjs: viewme.md 6 MD025/single-title/single-h1␊ + mjs: viewme.md 12 MD019/no-multiple-space-atx␊ mjs: viewme.md 14 MD047/single-trailing-newline`, stdout: `markdownlint-cli2 vX.Y.Z (markdownlint vX.Y.Z)␊ Finding: **/*.md␊ diff --git a/test/snapshots/markdownlint-cli2-test-exec.mjs.snap b/test/snapshots/markdownlint-cli2-test-exec.mjs.snap index 3250a6be..b09248bc 100644 Binary files a/test/snapshots/markdownlint-cli2-test-exec.mjs.snap and b/test/snapshots/markdownlint-cli2-test-exec.mjs.snap differ diff --git a/test/snapshots/markdownlint-cli2-test-main.mjs.md b/test/snapshots/markdownlint-cli2-test-main.mjs.md index 31e4a3b6..b0d52be9 100644 --- a/test/snapshots/markdownlint-cli2-test-main.mjs.md +++ b/test/snapshots/markdownlint-cli2-test-main.mjs.md @@ -2919,6 +2919,7 @@ Generated by [AVA](https://avajs.dev). dir3/hr.md:2 second-line Rule that reports an error for the second line␊ dir3/hr.md:3 sample-rule-commonjs Sample rule (commonjs) [Sample error for hr]␊ dir3/hr.md:3 sample-rule-module Sample rule (module) [Sample error for hr]␊ + dir4/hr.md:3 sample-rule-commonjs Sample rule (commonjs) [Sample error for hr]␊ dir4/hr.md:3 sample-rule-module Sample rule (module) [Sample error for hr]␊ viewme.md:1 every-n-lines Rule that reports an error every N lines [This rule threw an exception: getLineMetadata is not a function]␊ viewme.md:1 first-line Rule that reports an error for the first line␊ @@ -2932,7 +2933,7 @@ Generated by [AVA](https://avajs.dev). stdout: `markdownlint-cli2 vX.Y.Z (markdownlint vX.Y.Z)␊ Finding: **/*.md␊ Linting: 9 file(s)␊ - Summary: 44 error(s)␊ + Summary: 45 error(s)␊ `, } @@ -4951,6 +4952,16 @@ Generated by [AVA](https://avajs.dev). mjs: viewme.md 6 MD025/single-title/single-h1␊ mjs: viewme.md 12 MD019/no-multiple-space-atx␊ mjs: viewme.md 14 MD047/single-trailing-newline␊ + cjs: viewme.md 3 MD009/no-trailing-spaces␊ + cjs: viewme.md 5 MD012/no-multiple-blanks␊ + cjs: viewme.md 6 MD025/single-title/single-h1␊ + cjs: viewme.md 12 MD019/no-multiple-space-atx␊ + cjs: viewme.md 14 MD047/single-trailing-newline␊ + mjs: viewme.md 3 MD009/no-trailing-spaces␊ + mjs: viewme.md 5 MD012/no-multiple-blanks␊ + mjs: viewme.md 6 MD025/single-title/single-h1␊ + mjs: viewme.md 12 MD019/no-multiple-space-atx␊ + mjs: viewme.md 14 MD047/single-trailing-newline␊ `, stdout: `markdownlint-cli2 vX.Y.Z (markdownlint vX.Y.Z)␊ Finding: **/*.md␊ diff --git a/test/snapshots/markdownlint-cli2-test-main.mjs.snap b/test/snapshots/markdownlint-cli2-test-main.mjs.snap index 1821d01c..f78e8cf1 100644 Binary files a/test/snapshots/markdownlint-cli2-test-main.mjs.snap and b/test/snapshots/markdownlint-cli2-test-main.mjs.snap differ