From 135760ce678c4e977cabb7d61ad98671723ccdb3 Mon Sep 17 00:00:00 2001 From: Cameron Little Date: Mon, 25 Jan 2021 19:10:12 +0100 Subject: [PATCH] Add support for custom rules dirs (#75) * Add support for custom rules dirs Resolves #68 * Changelog * Add a bunch of test files for custom rules * Work around directory mangling issue * add link to eslint config docs * Improve console output when linting fails * changelog * format --- ESLint.novaextension/CHANGELOG.md | 10 ++++++ ESLint.novaextension/extension.json | 26 ++++++++++++--- package.json | 1 + src/getRulesDirs.ts | 35 ++++++++++++++++++++ src/process.ts | 46 +++++++++++++++++++++----- test/rules-1/.eslintrc.js | 7 ++++ test/rules-1/README.md | 1 + test/rules-1/custom-rules/index.js | 13 ++++++++ test/rules-1/custom-rules/package.json | 6 ++++ test/rules-1/index.js | 3 ++ test/rules-2/.eslintrc.js | 7 ++++ test/rules-2/README.md | 7 ++++ test/rules-2/index.js | 3 ++ yarn.lock | 3 ++ 14 files changed, 155 insertions(+), 13 deletions(-) create mode 100644 src/getRulesDirs.ts create mode 100644 test/rules-1/.eslintrc.js create mode 100644 test/rules-1/README.md create mode 100644 test/rules-1/custom-rules/index.js create mode 100644 test/rules-1/custom-rules/package.json create mode 100644 test/rules-1/index.js create mode 100644 test/rules-2/.eslintrc.js create mode 100644 test/rules-2/README.md create mode 100644 test/rules-2/index.js diff --git a/ESLint.novaextension/CHANGELOG.md b/ESLint.novaextension/CHANGELOG.md index 4f4054fe..e56bca90 100644 --- a/ESLint.novaextension/CHANGELOG.md +++ b/ESLint.novaextension/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## future + +### Added + +- Support custom rules directories + +### Changed + +- Improved Preferences meta-info + ## v1.6.0 ### Added diff --git a/ESLint.novaextension/extension.json b/ESLint.novaextension/extension.json index d1e228ed..3a8c643c 100644 --- a/ESLint.novaextension/extension.json +++ b/ESLint.novaextension/extension.json @@ -34,13 +34,21 @@ }, { "key": "apexskier.eslint.config.eslintPath", - "title": "Path to ESLint executable", + "title": "ESLint executable", "type": "path" }, { "key": "apexskier.eslint.config.eslintConfigPath", - "title": "Path to ESLint configuration", + "title": "ESLint configuration", "type": "path" + }, + { + "key": "apexskier.eslint.config.eslintRulesDirs", + "title": "Rules directories", + "link": "https://eslint.org/docs/user-guide/command-line-interface#-rulesdir", + "type": "pathArray", + "allowFiles": false, + "allowFolders": true } ], @@ -54,15 +62,25 @@ }, { "key": "apexskier.eslint.config.eslintPath", - "title": "Path to ESLint executable", + "title": "ESLint executable", "type": "path", "relative": true }, { "key": "apexskier.eslint.config.eslintConfigPath", - "title": "Path to ESLint configuration", + "title": "ESLint configuration", + "link": "https://eslint.org/docs/user-guide/configuring#configuration-file-formats", "type": "path", "relative": true + }, + { + "key": "apexskier.eslint.config.eslintRulesDirs", + "title": "Rules directories", + "link": "https://eslint.org/docs/user-guide/command-line-interface#-rulesdir", + "type": "pathArray", + "relative": true, + "allowFiles": false, + "allowFolders": true } ], diff --git a/package.json b/package.json index 170a30f4..7e75078f 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "concurrently": "^5.3.0", "eslint": "^7.18.0", "eslint-config-prettier": "^7.2.0", + "eslint-plugin-custom-rules": "file:./test/rules-1/custom-rules", "eslint-plugin-html": "^6.1.1", "eslint-plugin-markdown": "^2.0.0-rc.1", "eslint-plugin-nova": "^1.3.0", diff --git a/src/getRulesDirs.ts b/src/getRulesDirs.ts new file mode 100644 index 00000000..309b2005 --- /dev/null +++ b/src/getRulesDirs.ts @@ -0,0 +1,35 @@ +// returns custom eslint rules directories +export function getRulesDirs(): Array | null { + const rulesDirs: Array = + nova.config.get("apexskier.eslint.config.eslintRulesDirs", "array") ?? []; + const workspaceRulesDirs = nova.workspace.config.get( + "apexskier.eslint.config.eslintRulesDirs", + "array" + ); + if (workspaceRulesDirs) { + for (const dir of workspaceRulesDirs) { + if (!dir.trim()) { + continue; + } + if (nova.path.isAbsolute(dir)) { + rulesDirs.push(dir); + } else if (nova.workspace.path) { + rulesDirs.push(nova.path.join(nova.workspace.path, dir)); + } else { + nova.workspace.showErrorMessage( + "Save your workspace before using a relative ESLint rules directories." + ); + return null; + } + } + } + + return ( + rulesDirs + .filter((d) => d.trim()) + // hack - JSON stringifying works around https://github.com/eslint/eslint/issues/14025 by forcing levn to parse as a string, not a regex + // I could try to strip the `/Volumes/Macintosh HD` from Nova's workspace dir, but that would have to be + // conditional, since global settings won't include it. This feels simpler, although it could break if eslint's options parsing changes + .map((d) => JSON.stringify(d)) + ); +} diff --git a/src/process.ts b/src/process.ts index 01930544..78d86385 100644 --- a/src/process.ts +++ b/src/process.ts @@ -1,9 +1,13 @@ import type { Linter, ESLint } from "eslint"; import { getEslintPath } from "./getEslintPath"; import { getEslintConfig } from "./getEslintConfig"; +import { getRulesDirs } from "./getRulesDirs"; let eslintPath: string | null = null; let eslintConfigPath: string | null = null; +let eslintRulesDirs: Array | null = null; + +// TODO: Clean up these disposables on deactivation nova.config.onDidChange("apexskier.eslint.config.eslintPath", async () => { eslintPath = await getEslintPath(); console.log("Updating ESLint executable globally", eslintPath); @@ -30,10 +34,24 @@ nova.workspace.config.onDidChange( nova.commands.invoke("apexskier.eslint.config.lintAllEditors"); } ); +nova.config.onDidChange("apexskier.eslint.config.eslintRulesDirs", () => { + eslintRulesDirs = getRulesDirs(); + console.log("Updating ESLint rules globally"); + nova.commands.invoke("apexskier.eslint.config.lintAllEditors"); +}); +nova.workspace.config.onDidChange( + "apexskier.eslint.config.eslintRulesDirs", + () => { + eslintRulesDirs = getRulesDirs(); + console.log("Updating ESLint rules for workspace"); + nova.commands.invoke("apexskier.eslint.config.lintAllEditors"); + } +); export async function initialize() { eslintPath = await getEslintPath(); eslintConfigPath = getEslintConfig(); + eslintRulesDirs = getRulesDirs(); } const syntaxToSupportingPlugins: { @@ -140,7 +158,12 @@ class ESLintProcess implements Disposable { const areLintErrors = status === 1; const noLintErrors = status === 0; if (!areLintErrors && !noLintErrors && !lintProcessWasTerminated) { - console.warn(stderr); + console.warn("Failed to lint"); + console.group(); + console.warn("stderr: ", stderr); + console.log("command: ", this._process.command); + console.log("args: ", ...(this._process.args ?? [])); + console.groupEnd(); throw new Error(`failed to lint (${status})`); } if (lintProcessWasTerminated) { @@ -167,6 +190,17 @@ class ESLintProcess implements Disposable { } } +function addConfigArguments(toArgs: Array) { + if (eslintRulesDirs) { + for (const dir of eslintRulesDirs) { + toArgs.unshift("--rulesdir", dir); + } + } + if (eslintConfigPath) { + toArgs.unshift("--config", eslintConfigPath); + } +} + export function runLintPass( content: string, path: string | null, @@ -185,7 +219,6 @@ export function runLintPass( return disposable; } const eslint = eslintPath; - const eslintConfig = eslintConfigPath; // remove file:/Volumes/Macintosh HD from uri const cleanPath = path ? "/" + decodeURI(path).split("/").slice(5).join("/") @@ -200,9 +233,7 @@ export function runLintPass( if (cleanPath) { args.unshift("--stdin-filename", cleanPath); } - if (eslintConfig) { - args.unshift("--config", eslintConfig); - } + addConfigArguments(args); const process = new ESLintProcess(eslint, args, callback); disposable.add(process); process.write(content); @@ -230,7 +261,6 @@ export function runFixPass( return disposable; } const eslint = eslintPath; - const eslintConfig = eslintConfigPath; // remove file:/Volumes/Macintosh HD from uri const cleanPath = "/" + decodeURI(path).split("/").slice(5).join("/"); @@ -241,9 +271,7 @@ export function runFixPass( } else { const args = ["--fix", "--format=json"]; args.unshift(cleanPath); - if (eslintConfig) { - args.unshift("--config", eslintConfig); - } + addConfigArguments(args); const process = new ESLintProcess(eslint, args, callback); disposable.add(process); } diff --git a/test/rules-1/.eslintrc.js b/test/rules-1/.eslintrc.js new file mode 100644 index 00000000..3936cf3e --- /dev/null +++ b/test/rules-1/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + plugins: ["custom-rules"], + extends: ["../../.eslintrc"], + rules: { + "custom-rules/test-rule": 1, + }, +}; diff --git a/test/rules-1/README.md b/test/rules-1/README.md new file mode 100644 index 00000000..dfc894b5 --- /dev/null +++ b/test/rules-1/README.md @@ -0,0 +1 @@ +`index.js` should have one warning (custom-rules/test-rule) diff --git a/test/rules-1/custom-rules/index.js b/test/rules-1/custom-rules/index.js new file mode 100644 index 00000000..bdbcf0e2 --- /dev/null +++ b/test/rules-1/custom-rules/index.js @@ -0,0 +1,13 @@ +module.exports = { + rules: { + "test-rule": { + create: function (context) { + return { + TemplateLiteral(node) { + context.report(node, "template literals show test-rule error"); + }, + }; + }, + }, + }, +}; diff --git a/test/rules-1/custom-rules/package.json b/test/rules-1/custom-rules/package.json new file mode 100644 index 00000000..3a03c60e --- /dev/null +++ b/test/rules-1/custom-rules/package.json @@ -0,0 +1,6 @@ +{ + "name": "eslint-plugin-custom-rules", + "version": "0.0.0", + "private": "true", + "main": "index.js" +} diff --git a/test/rules-1/index.js b/test/rules-1/index.js new file mode 100644 index 00000000..d27b8e77 --- /dev/null +++ b/test/rules-1/index.js @@ -0,0 +1,3 @@ +export function test(foo) { + console.log(`hello ${foo}`); +} diff --git a/test/rules-2/.eslintrc.js b/test/rules-2/.eslintrc.js new file mode 100644 index 00000000..3936cf3e --- /dev/null +++ b/test/rules-2/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + plugins: ["custom-rules"], + extends: ["../../.eslintrc"], + rules: { + "custom-rules/test-rule": 1, + }, +}; diff --git a/test/rules-2/README.md b/test/rules-2/README.md new file mode 100644 index 00000000..149af0c6 --- /dev/null +++ b/test/rules-2/README.md @@ -0,0 +1,7 @@ +`index.js` should have one warning (custom-rules/test-rule) when the project has custom rules configured: + +``` +"apexskier.eslint.config.eslintRulesDirs" : [ + "test\/rules-1\/custom-rules" +], +``` diff --git a/test/rules-2/index.js b/test/rules-2/index.js new file mode 100644 index 00000000..d27b8e77 --- /dev/null +++ b/test/rules-2/index.js @@ -0,0 +1,3 @@ +export function test(foo) { + console.log(`hello ${foo}`); +} diff --git a/yarn.lock b/yarn.lock index 72951d4d..78cc6011 100644 --- a/yarn.lock +++ b/yarn.lock @@ -575,6 +575,9 @@ eslint-config-prettier@^7.2.0: resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz#f4a4bd2832e810e8cc7c1411ec85b3e85c0c53f9" integrity sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg== +"eslint-plugin-custom-rules@file:./test/rules-1/custom-rules": + version "0.0.0" + eslint-plugin-html@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-6.1.1.tgz#95aee151900b9bb2da5fa017b45cc64456a0a74e"