From 4cd45aabcffc5adeb339703bb8c14e3069ba3de8 Mon Sep 17 00:00:00 2001 From: Brody McKee Date: Wed, 11 Aug 2021 13:37:55 +0300 Subject: [PATCH] Add rootDir setting to eslint-plugin-next (#27918) ## Introduction This PR enables setting a `rootDir` for a Next.js project, and follows the same pattern used by [`@typescript-eslint/parser`](https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser#parseroptionsproject). ## Details Previously, users had to pass paths to the rule itself. ```js module.exports = { rules: { "@next/next/no-html-link-for-pages": [ "error", // This could be a string, or array of strings. "/packages/my-app/pages", ], }, }; ``` With this PR, this has been simplified (the previous implementation still works as expected). ```js module.exports = { settings: { next: { rootDir: "/packages/my-app", }, }, rules: { "@next/next/no-html-link-for-pages": "error", }, }; ``` Further, this rule allows the use of globs, again aligning with `@typescript-eslint/parser`. ```js module.exports = { settings: { next: { // Globs rootDir: "/packages/*", rootDir: "/packages/{app-a,app-b}", // Arrays rootDir: ["/app-a", "/app-b"], // Arrays with globs rootDir: ["/main-app", "/other-apps/*"], }, }; ``` This enables users to either provide per-workspace configuration with overrides, or to use globs for situations like monorepos where the apps share a domain (micro-frontends). This doesn't solve, but improves https://github.com/vercel/next.js/issues/26330. ## Feature - [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [x] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [x] Make sure the linting passes --- packages/eslint-plugin-next/README.md | 4 ++ packages/eslint-plugin-next/jsconfig.json | 7 ++++ .../lib/rules/google-font-display.js | 2 +- .../lib/rules/google-font-preconnect.js | 2 +- .../lib/rules/link-passhref.js | 2 +- .../lib/rules/no-html-link-for-pages.js | 31 ++++++++++---- .../lib/rules/no-page-custom-font.js | 2 +- .../lib/utils/get-root-dirs.js | 40 +++++++++++++++++++ .../{nodeAttributes.js => node-attributes.js} | 0 packages/eslint-plugin-next/package.json | 9 +++++ yarn.lock | 22 +++++++--- 11 files changed, 104 insertions(+), 17 deletions(-) create mode 100644 packages/eslint-plugin-next/README.md create mode 100644 packages/eslint-plugin-next/jsconfig.json create mode 100644 packages/eslint-plugin-next/lib/utils/get-root-dirs.js rename packages/eslint-plugin-next/lib/utils/{nodeAttributes.js => node-attributes.js} (100%) diff --git a/packages/eslint-plugin-next/README.md b/packages/eslint-plugin-next/README.md new file mode 100644 index 0000000000000..4e4f393c3a0d4 --- /dev/null +++ b/packages/eslint-plugin-next/README.md @@ -0,0 +1,4 @@ +# `@next/eslint-plugin-next` + +Documentation for `@next/eslint-plugin-next` can be found at: +https://nextjs.org/docs/basic-features/eslint#eslint-plugin diff --git a/packages/eslint-plugin-next/jsconfig.json b/packages/eslint-plugin-next/jsconfig.json new file mode 100644 index 0000000000000..e4468a2aee6de --- /dev/null +++ b/packages/eslint-plugin-next/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2019" + }, + "exclude": ["node_modules"] +} diff --git a/packages/eslint-plugin-next/lib/rules/google-font-display.js b/packages/eslint-plugin-next/lib/rules/google-font-display.js index d59dde1021fc4..1dc26c5ed7566 100644 --- a/packages/eslint-plugin-next/lib/rules/google-font-display.js +++ b/packages/eslint-plugin-next/lib/rules/google-font-display.js @@ -1,4 +1,4 @@ -const NodeAttributes = require('../utils/nodeAttributes.js') +const NodeAttributes = require('../utils/node-attributes.js') module.exports = { meta: { diff --git a/packages/eslint-plugin-next/lib/rules/google-font-preconnect.js b/packages/eslint-plugin-next/lib/rules/google-font-preconnect.js index 6d5aed8bcc431..930545ae8994b 100644 --- a/packages/eslint-plugin-next/lib/rules/google-font-preconnect.js +++ b/packages/eslint-plugin-next/lib/rules/google-font-preconnect.js @@ -1,4 +1,4 @@ -const NodeAttributes = require('../utils/nodeAttributes.js') +const NodeAttributes = require('../utils/node-attributes.js') module.exports = { meta: { diff --git a/packages/eslint-plugin-next/lib/rules/link-passhref.js b/packages/eslint-plugin-next/lib/rules/link-passhref.js index af0f35596c7db..dc41a44051662 100644 --- a/packages/eslint-plugin-next/lib/rules/link-passhref.js +++ b/packages/eslint-plugin-next/lib/rules/link-passhref.js @@ -1,4 +1,4 @@ -const NodeAttributes = require('../utils/nodeAttributes.js') +const NodeAttributes = require('../utils/node-attributes.js') module.exports = { meta: { diff --git a/packages/eslint-plugin-next/lib/rules/no-html-link-for-pages.js b/packages/eslint-plugin-next/lib/rules/no-html-link-for-pages.js index f79d6667abac8..c1a6eda644481 100644 --- a/packages/eslint-plugin-next/lib/rules/no-html-link-for-pages.js +++ b/packages/eslint-plugin-next/lib/rules/no-html-link-for-pages.js @@ -1,5 +1,7 @@ +// @ts-check const path = require('path') const fs = require('fs') +const getRootDir = require('../utils/get-root-dirs') const { getUrlFromPagesDirectories, normalizeURL, @@ -20,7 +22,7 @@ const fsExistsSyncCache = {} module.exports = { meta: { docs: { - description: 'Prohibit full page refresh for nextjs pages', + description: 'Prohibit full page refresh for Next.js pages', category: 'HTML', recommended: true, }, @@ -43,14 +45,27 @@ module.exports = { ], }, + /** + * Creates an ESLint rule listener. + * + * @param {import('eslint').Rule.RuleContext} context - ESLint rule context + * @returns {import('eslint').Rule.RuleListener} An ESLint rule listener + */ create: function (context) { - const [customPagesDirectory] = context.options - const pagesDirs = customPagesDirectory - ? [customPagesDirectory].flat() - : [ - path.join(context.getCwd(), 'pages'), - path.join(context.getCwd(), 'src', 'pages'), - ] + /** @type {(string|string[])[]} */ + const ruleOptions = context.options + const [customPagesDirectory] = ruleOptions + + const rootDirs = getRootDir(context) + + const pagesDirs = (customPagesDirectory + ? [customPagesDirectory] + : rootDirs.map((dir) => [ + path.join(dir, 'pages'), + path.join(dir, 'src', 'pages'), + ]) + ).flat() + const foundPagesDirs = pagesDirs.filter((dir) => { if (fsExistsSyncCache[dir] === undefined) { fsExistsSyncCache[dir] = fs.existsSync(dir) diff --git a/packages/eslint-plugin-next/lib/rules/no-page-custom-font.js b/packages/eslint-plugin-next/lib/rules/no-page-custom-font.js index ec7971693d3ac..94ecaba8ee1b3 100644 --- a/packages/eslint-plugin-next/lib/rules/no-page-custom-font.js +++ b/packages/eslint-plugin-next/lib/rules/no-page-custom-font.js @@ -1,4 +1,4 @@ -const NodeAttributes = require('../utils/nodeAttributes.js') +const NodeAttributes = require('../utils/node-attributes.js') module.exports = { meta: { diff --git a/packages/eslint-plugin-next/lib/utils/get-root-dirs.js b/packages/eslint-plugin-next/lib/utils/get-root-dirs.js new file mode 100644 index 0000000000000..9a65aa99201b8 --- /dev/null +++ b/packages/eslint-plugin-next/lib/utils/get-root-dirs.js @@ -0,0 +1,40 @@ +// @ts-check +const glob = require('glob') + +/** + * Process a Next.js root directory glob. + * + * @param {string} rootDir - A Next.js root directory glob. + * @returns {string[]} - An array of Root directories. + */ +const processRootDir = (rootDir) => { + // Ensures we only match folders. + if (!rootDir.endsWith('/')) rootDir += '/' + return glob.sync(rootDir) +} + +/** + * Gets one or more Root + * + * @param {import('eslint').Rule.RuleContext} context - ESLint rule context + * @returns An array of root directories. + */ +const getRootDirs = (context) => { + let rootDirs = [context.getCwd()] + + /** @type {{rootDir?:string|string[]}|undefined} */ + const nextSettings = context.settings.next || {} + let rootDir = nextSettings.rootDir + + if (typeof rootDir === 'string') { + rootDirs = processRootDir(rootDir) + } else if (Array.isArray(rootDir)) { + rootDirs = rootDir + .map((dir) => (typeof dir === 'string' ? processRootDir(dir) : [])) + .flat() + } + + return rootDirs +} + +module.exports = getRootDirs diff --git a/packages/eslint-plugin-next/lib/utils/nodeAttributes.js b/packages/eslint-plugin-next/lib/utils/node-attributes.js similarity index 100% rename from packages/eslint-plugin-next/lib/utils/nodeAttributes.js rename to packages/eslint-plugin-next/lib/utils/node-attributes.js diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 0b85d4968780e..eba91f0aae912 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -7,5 +7,14 @@ "repository": { "url": "vercel/next.js", "directory": "packages/eslint-plugin-next" + }, + "files": [ + "lib" + ], + "dependencies": { + "glob": "7.1.7" + }, + "devDependencies": { + "@types/eslint": "7.28.0" } } diff --git a/yarn.lock b/yarn.lock index 3b5433efe116c..32c3b2ae84ab1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4142,10 +4142,10 @@ "@types/eslint" "*" "@types/estree" "*" -"@types/eslint@*": - version "7.2.8" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.8.tgz#45cd802380fcc352e5680e1781d43c50916f12ee" - integrity sha512-RTKvBsfz0T8CKOGZMfuluDNyMFHnu5lvNr4hWEsQeHXH6FcmIDIozOyWMh36nLGMwVd5UFNXC2xztA8lln22MQ== +"@types/eslint@*", "@types/eslint@7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.28.0.tgz#7e41f2481d301c68e14f483fe10b017753ce8d5a" + integrity sha512-07XlgzX0YJUn4iG1ocY4IX9DzKSmMGUs6ESKlxWhZRaa0fatIWaHWUVapcuGa8r5HFnTqzj+4OCjd5f7EZ/i/A== dependencies: "@types/estree" "*" "@types/json-schema" "*" @@ -6325,7 +6325,7 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001165, caniuse-lite@^1.0.30001202, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001228: +caniuse-lite@1.0.30001228, caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001020, caniuse-lite@^1.0.30001093, caniuse-lite@^1.0.30001165, caniuse-lite@^1.0.30001202, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001228: version "1.0.30001228" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz#bfdc5942cd3326fa51ee0b42fbef4da9d492a7fa" integrity sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A== @@ -9826,6 +9826,18 @@ glob@7.1.6, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glo once "^1.3.0" path-is-absolute "^1.0.0" +glob@7.1.7: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-dirs@^2.0.1: version "2.1.0" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.1.0.tgz#e9046a49c806ff04d6c1825e196c8f0091e8df4d"