From 4753d26b949a521c48334882c6bac4eb6cd2a2c3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Feb 2023 16:42:23 +0100 Subject: [PATCH] eslint-plugin: add no-relative-monorepo-imports rule Signed-off-by: Patrik Oldsberg --- packages/eslint-plugin/index.js | 2 + packages/eslint-plugin/lib/getPackages.js | 10 ++- .../rules/no-relative-monorepo-imports.js | 80 +++++++++++++++++++ .../rules/no-undeclared-imports.js | 4 +- 4 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 packages/eslint-plugin/rules/no-relative-monorepo-imports.js diff --git a/packages/eslint-plugin/index.js b/packages/eslint-plugin/index.js index cf1dd20ab7..20a0ff2d18 100644 --- a/packages/eslint-plugin/index.js +++ b/packages/eslint-plugin/index.js @@ -20,12 +20,14 @@ module.exports = { plugins: ['@backstage'], rules: { '@backstage/no-forbidden-package-imports': 'error', + '@backstage/no-relative-monorepo-imports': 'error', '@backstage/no-undeclared-imports': 'error', }, }, }, rules: { 'no-forbidden-package-imports': require('./rules/no-forbidden-package-imports'), + 'no-relative-monorepo-imports': require('./rules/no-relative-monorepo-imports'), 'no-undeclared-imports': require('./rules/no-undeclared-imports'), }, }; diff --git a/packages/eslint-plugin/lib/getPackages.js b/packages/eslint-plugin/lib/getPackages.js index 318d0654c2..6c8c1a66b2 100644 --- a/packages/eslint-plugin/lib/getPackages.js +++ b/packages/eslint-plugin/lib/getPackages.js @@ -16,7 +16,7 @@ // @ts-check -/** @type {import('@manypkg/get-packages')} */ +const path = require('path'); const manypkg = require('@manypkg/get-packages'); /** @@ -31,6 +31,7 @@ const manypkg = require('@manypkg/get-packages'); * @property {ExtendedPackage} root * @property {ExtendedPackage[]} list * @property {Map} map + * @property {(path: string) => ExtendedPackage | undefined} byPath */ // Loads all packages in the monorepo once, and caches the result @@ -56,6 +57,13 @@ module.exports = (function () { map: new Map(packages.packages.map(pkg => [pkg.packageJson.name, pkg])), list: packages.packages, root: packages.root, + byPath(filePath) { + return packages.packages.find(pkg => { + if (!path.relative(pkg.dir, filePath).startsWith('..')) { + return pkg; + } + }); + }, }; lastLoadAt = Date.now(); return result; diff --git a/packages/eslint-plugin/rules/no-relative-monorepo-imports.js b/packages/eslint-plugin/rules/no-relative-monorepo-imports.js new file mode 100644 index 0000000000..cb18eb506c --- /dev/null +++ b/packages/eslint-plugin/rules/no-relative-monorepo-imports.js @@ -0,0 +1,80 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +const path = require('path'); +const visitImports = require('../lib/visitImports'); +const getPackageMap = require('../lib/getPackages'); + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + messages: { + outside: 'Import of {{path}} is outside of any known monorepo package', + forbidden: + "Relative imports of monorepo packages are forbidden, use '{{newImport}}' instead", + }, + }, + create(context) { + const packages = getPackageMap(context.getCwd()); + if (!packages) { + return {}; + } + const filePath = context.getPhysicalFilename + ? context.getPhysicalFilename() + : context.getFilename(); + + const localPkg = packages.byPath(filePath); + if (!localPkg) { + return {}; + } + + return visitImports(context, (node, imp) => { + if (imp.type !== 'local') { + return; + } + + const target = path.resolve(path.dirname(filePath), imp.path); + if (!path.relative(localPkg.dir, target).startsWith('..')) { + return; + } + + const targetPkg = packages.byPath(target); + if (!targetPkg) { + context.report({ + node: node, + messageId: 'outside', + data: { + path: target, + }, + }); + return; + } + + const targetPath = path.relative(targetPkg.dir, target); + const targetName = targetPkg.packageJson.name ?? ''; + context.report({ + node: node, + messageId: 'forbidden', + data: { + newImport: targetPath ? `${targetName}/${targetPath}` : targetName, + }, + }); + }); + }, +}; diff --git a/packages/eslint-plugin/rules/no-undeclared-imports.js b/packages/eslint-plugin/rules/no-undeclared-imports.js index f7f7b17354..4a8756b71e 100644 --- a/packages/eslint-plugin/rules/no-undeclared-imports.js +++ b/packages/eslint-plugin/rules/no-undeclared-imports.js @@ -135,9 +135,7 @@ module.exports = { ? context.getPhysicalFilename() : context.getFilename(); - const localPkg = packages.list.find(p => - filePath.startsWith(p.dir + path.sep), - ); + const localPkg = packages.byPath(filePath); if (!localPkg) { return {}; }