eslint-plugin-boundaries
用 Robert C. Martin 的话来说,"软件架构是划线的艺术,我称之为边界。这些边界将软件元素彼此分开,并限制它们的一方不知另一方的存在。"
这个插件确保你的架构边界得到了项目中的各个元素的尊重,通过检查文件夹和文件结构及其依赖关系。 它并不是 eslint-plugin-import 的替代品,相反,推荐将这两个插件结合使用。
默认情况下,该插件通过检查 import
语句起作用,但它也能够分析 "require"、"exports" 和动态导入,并可以配置为检查任何其他 AST 节点。(阅读 主要规则概述 和 配置 章节以更好理解)
目录
详细信息
安装
该模块通过 npm 分发,bundled with node,并应安装为项目的 devDependencies 之一:
npm install --save-dev eslint eslint-plugin-boundaries
eslint-plugin-boundaries
不会为你安装 eslint
。你必须自己安装它。
在你的 .eslintrc.(yml|json|js)
中激活插件和一个现成的配置:
{
"plugins": ["boundaries"],
"extends": ["plugin:boundaries/recommended"]
}
Eslint v9 及以上版本
从 5.0.0-beta.0 版本开始,你必须使用这个插件的 beta 版本才能与 eslint v9 及以上版本一起使用。兼容性还不完全保证。如果你遇到任何问题,请提出 issue。
要安装最新的 beta 版本,你可以使用以下命令:
npm install --save-dev eslint-plugin-boundaries@beta
beta eslint v9 beta 版本在 release-eslint-v9
分支 中维护。一旦我们充分测试了预定义配置、示例以及与其他 eslint 插件(例如使用 TypeScript 时需要的插件)和 eslint v9 的兼容性,我们将发布稳定版本。
概述
所有插件规则都需要能够识别项目中的元素,因此,首先你要使用 boundaries/elements
设置定义项目元素类型。
插件将使用提供的模式将每个文件识别为某个元素类型。它还将为在 依赖节点(import
或其他语句) 中检测到的每个依赖项分配一个类型,并检查依赖元素之间的关系是否被允许。
{
"settings": {
"boundaries/elements": [
{
"type": "helpers",
"pattern": "helpers/*"
},
{
"type": "components",
"pattern": "components/*"
},
{
"type": "modules",
"pattern": "modules/*"
}
]
}
}
这只是一个基本的配置示例。插件可以配置为识别作为文件的元素,或包含文件的文件夹元素。它还支持捕获路径片段以在每项规则选项中用于后面配置等。阅读 配置章节 以获取更多信息,因为正确配置它对于利用插件的所有功能至关重要。
一旦定义了项目元素类型,你可以使用它们来配置每个规则及其选项。例如,你可以通过配置 element-types
规则来定义哪些元素可以是其它元素的依赖:
{
"rules": {
"boundaries/element-types": [2, {
"default": "disallow",
"rules": [
{
"from": "components",
"allow": ["helpers", "components"]
},
{
"from": "modules",
"allow": ["helpers", "components", "modules"]
}
]
}]
}
}
当插件未识别文件或依赖项的元素类型时,不会将规则应用到它们,但你可以通过启用 boundaries/no-unknown-files 规则来强制项目中的所有文件都属于某个元素类型。
主要规则概述
允许的元素类型
该规则确保项目元素类型之间的依赖关系是允许的。
使用示例:
- 定义项目中的类型 "models"、"views" 和 "controllers"。然后确保 "views" 和 "models" 只能被 "controllers" 导入,且 "controllers" 不会被 "views" 或 "models" 使用。
- 定义项目中的类型 "components"、"views"、"layouts"、"pages"、"helpers"。然后确保 "components" 只能导入 "helpers","views" 只能导入 "components" 或 "helpers","layouts" 只能导入 "views"、"components" 或 "helpers",而 "pages" 可以导入任何其他元素类型。
阅读 boundaries/element-types
规则的文档 以获取更多信息。
允许的外部模块
可以使用该规则检查项目中每种类型的元素使用的外部依赖项。例如,你可以定义 "helpers" 不能导入 react
,或者 "components" 不能导入 react-router-dom
,或 modules 不能导入 { Link } from react-router-dom
。
阅读 boundaries/external
规则的文档 以获取更多信息。
私有元素
该规则确保元素不能引入其他元素的子元素。因此,当元素 B 是 A 的子元素时,B 将成为 A 的"私有"元素,只有 A 可以使用它。
阅读 boundaries/no-private
规则的文档 以获取更多信息。
入口点
此规则确保元素不能从其他元素中导入除该类型定义的入口点(默认情况下为 index.js
)之外的任何文件。
阅读 boundaries/entry-point
规则的文档 以获取更多信息。
规则
- boundaries/element-types:检查元素类型之间允许的依赖关系
- boundaries/external:按元素类型检查允许的外部依赖项
- boundaries/entry-point: 检查每种元素类型使用的入口点
- boundaries/no-private: 防止引入另一元素的私有元素
- boundaries/no-unknown: 防止从已知元素引入未知元素
- boundaries/no-ignored: 防止从识别的元素引入忽略的文件
- boundaries/no-unknown-files: 防止创建未被识别为任何元素类型的文件
配置
全局设置
boundaries/element-types
定义匹配模式以将项目中的每个文件识别为某种元素类型。所有规则都需要正确配置此设置才能正常工作。插件尝试识别被分析的每个文件或规则中的 import
语句为定义的元素类型之一。分配的元素类型将是第一个匹配模式的类型,顺序与数组中定义的元素相同,因此 你应该将它们从最准确的模式排序到最不准确的模式。 每个 element
的属性:
type
:<string>
将类型分配给匹配pattern
的文件或导入。这种类型将在后续规则配置中使用。pattern
:<string>|<array>
micromatch
模式。默认情况下,插件将尝试从文件路径的右侧逐步匹配此模式。 这意味着你不必定义从 base 项目路径匹配的模式,而只需定义你想要匹配的路径的最后一部分。这是因为插件支持作为其他元素子元素的元素,并且否则它可能错误地将子元素识别为父元素的一部分。
例如,给定路径src/helpers/awesome-helper/index.js
,插件将尝试匹配模式index.js
,然后是awesome-helper/index.js
,然后是helpers/awesome-helper/index.js
,依此类推。一旦匹配到模式,它就会分配相应的元素类型,并继续按照相同的逻辑搜索父元素,直到完全分析完整路径。此行为可以通过将mode
选项设置为full
来禁用,则提供的模式将尝试匹配全路径。basePattern
:<string>
可选micromatch
模式。如果提供,则元素路径的左侧必须从项目根路径开始也匹配此模式(例如模式为[basePattern]/**/[pattern]
)。此选项在将mode
选项设置为file
或folder
值时特别有用,但仍需从完整路径的其余部分捕获片段(请参阅下面的baseCapture
选项)。mode
:<string> file|folder|full
可选。- 当它设置为
folder
(默认值)时,插件将分配元素类型给第一个与模式匹配的文件的父文件夹。在实践中,这相当于将给定模式添加**/*
,但插件会自行处理,因为它需要确切地知道哪个父文件夹应该被视为元素。 - 如果它设置为
file
,则给定模式不会被修改,但插件仍会尝试匹配路径的最后部分。因此,一个模式如*.model.js
将与路径src/foo.model.js
、src/modules/foo/foo.model.js
、src/modules/foo/models/foo.model.js
,等匹配。 - 如果它设置为
full
,则给定模式将仅匹配完全匹配的路径。这意味着你将必须提供从 base 项目路径匹配的模式。因此,为了匹配src/modules/foo/foo.model.js
,你将必须提供像**/*.model.js
、**/*/*.model.js
、src/*/*/*.model.js
等模式。(选择的模式将取决于你想捕获路径的哪个部分)
- 当它设置为
capture
:<array>
可选。这是插件的一个非常强大的功能。它允许捕获匹配路径中的一些片段值,以便稍后在规则配置中使用。它在底层使用micromatch
捕获功能,并将每个值存储在与捕获数组相同索引的对象的capture
键中。
例如,给定pattern: "helpers/*/*.js"
,capture: ["category", "elementName"]
和路径helpers/data/parsers.js
,结果将是{ category: "data", elementName: "parsers" }
。baseCapture
:<array>
可选。micromatch
模式。它允许像capture
捕捉基础路径basePattern
的值一样捕捉路径片段。所有从capture
和baseCapture
捕获的键和值可以在规则配置中使用。
{
"settings": {
"boundaries/elements": [
{
"type": "helpers",
"pattern": "helpers/*/*.js",
"mode": "file",
"capture": ["category", "elementName"]
},
{
"type": "components",
"pattern": "components/*/*",
"capture": ["family", "elementName"]
},
{
"type": "modules",
"pattern": "module/*",
"capture": ["elementName"]
}
]
}
}
提示: 你可以在配置插件时启用 调试模式,你将获得关于项目中每个文件分配的类型的信息,以及捕获的属性和值。
boundaries/dependency-nodes
此设置允许修改内置的默认依赖节点。默认情况下,插件仅会分析 import
语句。所有插件规则都将适用于此设置中定义的节点。
该设置应为以下字符串的数组:
'require'
: 分析require
语句。'import'
: 分析import
语句。'export'
: 分析export
语句。'dynamic-import'
: 分析 动态导入 语句。
如果你想定义自定义的依赖节点,例如 jest.mock(...)
,请使用 additional-dependency-nodes 设置。
例如,如果你想分析import
和dynamic-import
语句,你应该使用以下值:
"boundaries/dependency-nodes": ["import", "dynamic-import"],
boundaries/additional-dependency-nodes
此设置允许定义自定义依赖节点进行分析。为插件定义的所有规则都将适用于此设置中定义的节点。
该设置应该是一个具有以下结构的对象数组:
selector
:依赖源定义的Literal
节点的 esquery 选择器。例如,要分析jest.mock(...)
调用,你可以使用这个 AST 选择器:CallExpression[callee.object.name=jest][callee.property.name=mock] > Literal:first-child
。kind
:依赖的种类,可能的值是:"value" 或 "type"。该选项仅在使用TypeScript时可用。
使用示例:
{
"boundaries/additional-dependency-nodes": [
// jest.requireActual('source')
{
"selector": "CallExpression[callee.object.name=jest][callee.property.name=requireActual] > Literal",
"kind": "value",
},
// jest.mock('source', ...)
{
"selector": "CallExpression[callee.object.name=jest][callee.property.name=mock] > Literal:first-child",
"kind": "value",
},
],
}
boundaries/include
插件将忽略不匹配这些 micromatch
模式 的文件或依赖项。如果未提供此选项,则将包含所有文件。
{
"settings": {
"boundaries/include": ["src/**/*.js"]
}
}
boundaries/ignore
插件将忽略匹配这些 micromatch
模式 的文件或依赖项。
{
"settings": {
"boundaries/ignore": ["**/*.spec.js", "src/legacy-code/**/*"]
}
}
注意:
boundaries/include
选项优先于boundaries/ignore
。如果你定义了boundaries/include
,可以使用boundaries/ignore
忽略已包含文件的子集。
boundaries/root-path
仅在从不同路径执行lint命令时遇到插件问题时使用此设置。
如何定义项目的根路径
默认情况下,插件使用当前工作目录 (process.cwd()
) 作为项目的根路径。当从规则和boundaries/elements
设置中解析文件匹配器时,此路径用作基本路径。尤其在使用basePattern
选项或boundaries/elements
设置中的full
模式时,这一点尤为重要。当从不同于项目根路径的路径执行lint命令时,这可能会产生意想不到的结果。要解决此问题,可以使用此选项定义不同的根路径。
例如,假设.eslintrc.js
文件位于项目根目录中,你可以如下定义根路径:
{
settings: {
"boundaries/root-path": path.resolve(__dirname)
}
}
注意,路径应在传递给插件之前绝对并已解析。否则,它将使用当前工作目录进行解析,问题仍然存在。如果你在.eslintrc.(yml|json)
文件中定义配置,并且不希望硬编码绝对路径,可以在执行lint命令时使用下一个环境变量定义根路径:
ESLINT_PLUGIN_BOUNDARIES_ROOT_PATH=../../project-root npm run lint
你也可以在环境变量中提供绝对路径,但使用相对于项目根目录的相对路径可能更有用。记住,它将从执行lint命令的路径进行解析。
预定义配置
插件分发时包含两种不同的预定义配置:“recommended”和“strict”。
推荐
如果你打算将插件应用于现有项目,建议使用此设置。boundaries/no-unknown
,boundaries/no-unknown-files
和boundaries/no-ignored
规则已禁用,因此允许项目的部分非合规的元素类型存在,允许逐步地重构代码。
{
"extends": ["plugin:boundaries/recommended"]
}
严格
启用所有规则,因此项目中的所有元素都将符合你的架构边界。😃
{
"extends": ["plugin:boundaries/strict"]
}
规则配置
一些规则需要额外的配置,并且必须在.eslintrc.(yml|json|js)
文件的每个特定rule
属性中定义。例如,允许的元素类型关系必须作为boundaries/element-types
规则的选项提供。如果在没有必要选项的情况下启用这些规则,将打印警告。
主要规则选项格式
每个规则的文档包含其自身选项的规范,但__主要规则共享选项定义的格式__。此处描述的格式对于element-types
,external
和entry-point
规则的选项都有效。
选项默认设置allow
或disallow
值,并提供规则数组。每个匹配的规则将覆盖默认值和以前匹配规则返回的值。因此,一旦为每种情况处理完选项,最终结果将是allow
或disallow
,该值将由插件规则应用于相应的方式,使其产生eslint错误或不产生。
{
"rules": {
"boundaries/element-types": [2, {
// 默认允许或不允许任何依赖性
"default": "allow",
// 为此规则定义自定义消息
"message": "${file.type} is not allowed to import ${dependency.type}",
"rules": [
{
// 在这种类型的文件中...
"from": ["helpers"],
// 禁止导入此类型的元素
"disallow": ["modules", "components"],
// 针对这种类型的导入(仅适用于使用 TypeScript 时)
"importKind": "value",
// 返回此自定义错误消息
"message": "Helpers must not import other thing than helpers"
},
{
"from": ["components"],
"disallow": ["modules"]
// 由于此规则没有“message”属性,因此将使用第一级别定义的消息
}
]
}]
}
}
请记住:
- 所有规则都被执行,并且结果值将是最后一个匹配规则返回的值。
- 如果一个规则同时包含
allow
和disallow
属性,disallow
属性具有优先级。如果disallow
匹配,将不会尝试匹配allow
。在这种情况下,该规则的结果将是disallow
。
规则选项属性
from/target
:<element matchers>
根据选项所在的规则,只会在分析文件匹配此元素匹配器(from
),或导入依赖项匹配此元素匹配器(target
)时应用规则。disallow/allow
:<value matchers>
如果插件规则目标与此匹配,则规则结果将是“disallow/allow”。每个规则将根据它要检查的内容在此处需要一种值。在element-types
规则的情况下,例如,需要提供另一个<element matcher>
以检查本地依赖项的类型。importKind
:<string>
可选。仅在使用TypeScript时有用,因为它允许定义规则在依赖项作为值或类型导入时是否适用。也可以定义为字符串数组或micromatch模式。请注意可能匹配的值为"value"
,"type"
或"typeof"
。例如,可以定义“组件”可以导入“助手”作为值,但不能作为类型。因此,import { helper } from "helpers/helper-a"
将被允许,但import type { Helper } from "helpers/helper-a"
将被禁止。message
:<string>
可选。如果规则导致错误,插件将返回此消息而不是默认消息。阅读错误消息了解更多信息。
提示:属性
from/target
和disallow/allow
可以接收单个匹配器或匹配器数组。
元素匹配器
规则选项中使用的元素匹配器可以具有以下格式:
- __
<string>
__:当元素类型与此micromatch
模式 匹配时返回true
。它 支持模板 用于从捕获值中使用值。 [<string>, <capturedValuesObject>]
:当元素类型与数组中的第一个元素匹配并且所有捕获的值也匹配时返回true
。
<capturedValuesObject>
必须是一个对象,其中包含来自元素的capture
键的键,和micromatch
模式 作为其值。(值也支持 模板)。
例如,对于类型为“helpers”的元素,其设置为:{ type: "helpers", pattern": "helpers/*/*.js", "capture": ["category", "elementName"]}
, 你可以编写如下元素匹配器:["helpers", { category: "data", elementName: "parsers"}]
:仅匹配类别为“data”和元素名称为“parsers”的助手元素 (helpers/data/parsers.js
)。["helpers", { category: "data" }]
:匹配所有类别为“data”的助手 (helpers/data/*.js
)["data-${from.elementName}", { category: "${from.category}" }]
:仅匹配类型等于加上“data-”前缀的导入文件的elementName
和类别等于导入文件的category
的助手元素。
模板
定义 元素匹配器 时,可替换从导入的元素(“from”)和导入的元素(“target”)捕获的值。它们在主字符串和 <capturedValuesObject>
中都被替换。
模板必须使用格式 ${from.CAPTURED_PROPERTY}
或 ${target.CAPTURED_PROPERTY}
定义。
错误消息
插件为每个规则返回不同的默认消息,检查每个规则的文档了解更多信息。但是一些规则支持在其配置中定义自定义消息,如在 "主要规则选项格式" 中看到的那样。
定义自定义消息时,可以提供关于当前文件或依赖项的信息。使用 ${file.PROPERTY}
或 ${dependency.PROPERTY}
,它将由文件或依赖项的相应捕获值替换:
{
"message": "${file.type}s of category ${file.category} are not allowed to import ${dependency.category}s"
// 如果错误是由于类型为“component”且捕获值“category”为“atom”的文件尝试导入类别为“molecule”的依赖项而产生,则消息将是:
// "components of category atom are not allowed to import molecules"
}
错误模板中可用的属性包括 file
或 dependency
分别有:
type
:元素的类型。internalPath
:分析或导入的文件路径。相对于元素的根路径。source
:仅对dependency
可用。导入语句的源代码。parent
:如果元素是另一个元素的子元素,此属性中也包含相应的type
,internalPath
和捕获属性。importKind
:仅在使用TypeScript时对dependency
可用。它包含正在分析的导入类型。可能的值有"value"
,"type"
或"typeof"
。- ...所有捕获的属性也可用
提示:阅读 "全局设置" 了解更多关于如何从元素捕获值的信息。
一些规则还提供了关于报告错误的额外信息。例如,no-external
规则提供了有关检测到的禁止说明符的信息。该信息可以使用${report.PROPERTY}
获取。检查每个规则的文档,了解它提供的报告属性:
{
"message": "Do not import ${report.specifiers} from ${dependency.source} in helpers"
}
规则配置的高级示例
为了说明插件支持的高定制化级别,这里是基于之前的全局elements
设置示例的boundaries/element-types
规则高级选项的示例:
```jsonc
{
"rules": {
"boundaries/element-types": [2, {
// 默认禁止导入任何元素
"default": "disallow",
"rules": [
{
// 允许从helpers文件中导入helpers文件
"from": ["helpers"],
"allow": ["helpers"]
},
{
// 文件位于“components”类型的元素内时
"from": ["components"],
"allow": [
// 允许导入同一家族的组件
["components", { "family": "${from.family}" }],
// 允许导入类别为“data”的helpers
["helpers", { "category": "data" }],
]
},
{
// 组件属于“molecule”家族时
"from": [["components", { "family": "molecule" }]],
"allow": [
// 允许导入属于“atom”家族的组件
["components", { "family": "atom" }],
],
},
{
// 组件属于“atom”家族时
"from": [["components", { "family": "atom" }]],
"disallow": [
// 禁止导入类别为“data”的helpers
["helpers", { "category": "data" }]
],
// 特定错误的自定义消息
"message": "Atom组件不能导入data helpers"
},
{
// 文件位于模块内时
"from": ["modules"],
"allow": [
// 允许导入任何类型的组件或helpers
"helpers",
"components"
]
},
{
// 模块名称以“page-”开头时
"from": [["modules", { "elementName": "page-*" }]],
"disallow": [
// 禁止导入非layout家族的任何类型组件
["components", { "family": "!layout" }],
],
// 特定错误的自定义消息
"message": "名称以'page-'开头的模块不能导入非layout组件。你试图从名称为${from.elementName}的模块中导入家族为${target.family}的组件"
}
]
}]
}
}
## 解析器
_"随着模块打包工具的出现以及当前模块和模块语法规范的发展,从'module'导入x应该查找哪个文件并不总是显而易见。"_ ([\**引用自`eslint-plugin-import`文档](#致谢))
此插件在内部使用`eslint-module-utils/resolve`模块,它是`eslint-plugin-import`插件的一部分。因此,__`import/resolver`设置也可以用于为此插件使用自定义解析器__。
[阅读`eslint-plugin-import`插件的`resolvers`章节以获取更多信息](https://github.com/benmosher/eslint-plugin-import#resolvers)。
```json
{
"settings": {
"import/resolver": {
"eslint-import-resolver-node": {},
"some-other-custom-resolver": { "someConfig": "value" }
}
}
}
与TypeScript一起使用
此插件也可以在使用@typescript-eslint/eslint-plugin
的TypeScript项目中使用。按照以下步骤配置它:
安装依赖:
npm i --save-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-import-resolver-typescript
在.eslintrc.js
配置文件中配置@typescript-eslint/parser
作为解析器,加载@typescript-eslint
插件,并设置eslint-import-resolver-typescript
解析器:
module.exports = {
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "boundaries"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:boundaries/recommended",
],
settings: {
"import/resolver": {
typescript: {
alwaysTryTypes: true,
},
},
},
};
请注意,
eslint-import-resolver-typescript
甚至可以检测到在tsconfig.json
文件中定义的自定义路径,因此它的使用也与此插件兼容。
如果您在配置过程中遇到任何问题,还可以用此存储库作为指南。它包含一个完全工作且经过测试的示例。
迁移指南
从v3.x迁移
新的v4.0.0版本引入了重大变化。如果您在使用v3.x,应当阅读“从v3迁移到v4”指南。
从v1.x迁移
新的v2.0.0版本引入了许多重大变化。如果您在使用v1.x,应当阅读“从v1迁移到v2”指南。
调试模式
为了帮助配置过程,插件可以跟踪分析的文件和导入信息。信息包括文件路径、分配的元素类型、捕获的值等。因此,它可以帮助您检查您的elements
设置是否按预期工作。您可以使用ESLINT_PLUGIN_BOUNDARIES_DEBUG
环境变量启用它。
ESLINT_PLUGIN_BOUNDARIES_DEBUG=1 npm run lint
致谢
* 引用自Robert C. Martin的书"清洁架构:软件结构与设计的工匠指南"。
** 此插件在内部使用eslint-plugin-import
插件的一部分eslint-module-utils/resolve
模块。感谢该插件的维护者们所做的出色工作。
贡献
许可证
MIT,请参阅LICENSE了解详细信息。