Custom eslint rule for disallowing cross-package references

Summary:
Added infra for writing and using custom eslint rules and created a rule for disallowing cross-package file imports. Such imports are anti-pattern and they also break standalone plugin bundling.

We still have a bunch of places where Flipper core references code directly from plugins. I've ignored these places for now and added task T71355623 to revisit them.

Reviewed By: passy

Differential Revision: D22998955

fbshipit-source-id: d04cff8fc115ba1300a7e6830306ec134046e927
This commit is contained in:
Anton Nikolaev
2020-08-07 10:19:34 -07:00
committed by Facebook GitHub Bot
parent 6989fa608d
commit 4a1c2a9ece
17 changed files with 428 additions and 72 deletions

View File

@@ -0,0 +1,110 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {resolve, dirname, join} from 'path';
import fs from 'fs-extra';
import {TSESTree} from '@typescript-eslint/experimental-utils';
import {createESLintRule} from '../utils/createEslintRule';
const rootDirs = new Map<string, string>();
function findRootDir(path: string): string {
const cachedRoot = rootDirs.get(path);
if (cachedRoot) {
return cachedRoot;
}
if (fs.pathExistsSync(join(path, 'package.json'))) {
rootDirs.set(path, path);
return path;
}
const parentRoot = findRootDir(dirname(path));
rootDirs.set(path, parentRoot);
return parentRoot;
}
type Options = [];
export type MessageIds = 'noRelativeImportsAcrossPackages';
export const RULE_NAME = 'no-relative-imports-across-packages';
export default createESLintRule<Options, MessageIds>({
name: RULE_NAME,
meta: {
type: 'problem',
docs: {
description: `Ensure that package boundaries are respected within monorepo`,
category: 'Possible Errors',
recommended: 'error',
},
schema: [],
messages: {
noRelativeImportsAcrossPackages: `Attempted to require "{{requiredPath}}" from file "{{filename}}" resolved to "{{resolvedPath}}" which is outside the package root dir "{{root}}".`,
},
},
defaultOptions: [],
create(context) {
const filename = context.getFilename();
const dir = dirname(filename);
const root = findRootDir(dir);
return {
ImportDeclaration(node: TSESTree.ImportDeclaration) {
if (typeof node.source.value === 'string') {
const requiredPath = node.source.value;
const resolvedPath = resolve(dir, requiredPath);
if (
// imported a file not a package
requiredPath.startsWith('.') &&
// the resolved path for the imported file is outside the package root
!resolvedPath.startsWith(root)
) {
context.report({
node,
messageId: 'noRelativeImportsAcrossPackages',
data: {
filename,
root,
requiredPath,
resolvedPath,
},
});
}
}
},
CallExpression(node: TSESTree.CallExpression) {
const args = node.arguments || [];
if (
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
args.length === 1 &&
args[0].type === 'Literal' &&
typeof args[0].value === 'string'
) {
const requiredPath = args[0].value;
const resolvedPath = resolve(dir, requiredPath);
if (
// required a file not a package
requiredPath.startsWith('.') &&
// the resolved path for the required file is outside the package root
!resolvedPath.startsWith(root)
) {
context.report({
node,
messageId: 'noRelativeImportsAcrossPackages',
data: {
filename,
root,
requiredPath,
resolvedPath,
},
});
}
}
},
};
},
});