在 Typescript 中防止不适当的导入和执行项目层次结构
Posted
技术标签:
【中文标题】在 Typescript 中防止不适当的导入和执行项目层次结构【英文标题】:Preventing inappropriate imports and enforcing project hierarchy in Typescript 【发布时间】:2020-07-29 04:45:24 【问题描述】:在 TS 项目中,我希望阻止以下内容:
来自文件夹common
的文件正在从文件夹 projectA
导入
来自文件夹projectB
的文件正在从文件夹projectA
导入
我希望允许以下内容:
文件夹projectA
中的文件正在从文件夹 common
导入。
我知道参考。但是,据我了解,它们需要构建用于类型检查(如果进行了这种分离,则必须先构建以创建 d.ts 文件),我宁愿避免这样做。
我有什么选择?是否可以简单地通过每个项目/文件夹的单独 tsconfig 文件来实现?
【问题讨论】:
由于构建依赖关系,您避免使用项目引用的理由有多大?查看my answer中的对位点 【参考方案1】:我建议使用 linter 来完成这项工作,无需调整构建步骤或使用项目参考。
eslint-plugin-import
是一个非常流行的 ESLint 插件,兼容 TS 并且可以为所欲为。配置 typescript-eslint 后(如果尚未完成),您可以使用以下规则:
让我们尝试以下项目结构:
| .eslintrc.js
| package.json
| tsconfig.json
\---src
+---common
| common.ts
|
+---projectA
| a.ts
|
\---projectB
b.ts
.eslintrc.js:
module.exports =
extends: ["plugin:import/typescript"],
parser: "@typescript-eslint/parser",
parserOptions:
sourceType: "module",
project: "./tsconfig.json",
,
plugins: ["@typescript-eslint", "import"],
rules:
"import/no-restricted-paths": [
"error",
basePath: "./src",
zones: [
// disallow import from projectB in common
target: "./common", from: "./projectB" ,
// disallow import from projectB in projectA
target: "./projectA", from: "./projectB" ,
],
,
],
"import/no-relative-parent-imports": "error",
,
;
每个 zone 都由 target 路径和 from 路径组成。目标是应用受限导入的路径。 from 路径定义了不允许在导入中使用的文件夹。
查看文件./src/common/common.ts
:
import a from "../projectA/a"; // works
// Error: Unexpected path "../projectB/b" imported in restricted zone.
import b from "../projectB/b";
import/no-relative-parent-imports
规则也抱怨这两种导入,比如a.ts
:
不允许从父目录进行相对导入。请在运行时传递您要导入的内容(依赖注入),将
common.ts
移动到与../projectA/a
相同的目录或考虑将../projectA/a
制作成一个包。
没有使用第三条规则import/no-internal-modules
,但我也在此处列出,因为它可以非常有用地限制对子文件夹/模块的访问,并在 TS 中模拟(至少)某种package internal modifier。
【讨论】:
这个很有意思,我明天试试。但是,Typescript 解决方案(如果有这样的解决方案没有额外的构建)应该更好,因为它只会显示可用/允许的自动完成导入。 >"不允许从父目录进行相对导入。"如果我有一个文件ProjectA/innerFolder/innerFolder/file.ts
尝试从projectA/consts.ts
导入,它会被阻止吗?
嗯,我怀疑,有一个直接的 TS 功能,它不允许某些模块导入模式(或者至少我不知道)。如果您需要更强的封装,通常会创建另一个 npm 包。对于自动完成:可能有多个配置,这会限制输入,但这将有其自身的组织成本,并且不会阻止模块解析/导入它们。
是的,eslint 会在导入路径中包含../
的所有内容上出错
@Inigo,我相信你写的是正确的。但是,如果您确实将 linter 视为工具链的重要组成部分,那么 1)所有错误都显示在编辑器中(取决于插件) 2)linter 在每次构建之前运行(取决于脚本)【参考方案2】:
TLDR;你真的应该只使用参考。这正是它们的用途。
但让我们先谈谈你的一些具体想法:
是否可以通过单独的 tsconfig 文件为每个项目/文件夹简单地实现?
是的,但它不是很灵活。您可以通过将其rootDir
设置为.
来隔离common。如果您尝试将 projectA 导入 common,则会收到 '/path/to/projectA' is not under 'rootDir'
错误。但是为了能够将 common 导入到 projectA,它的 rootDir
必须更加全局,但这样您就可以导入 projectB.
不仅如此,根据Project References 文档:
以前,如果您使用单个 tsconfig 文件,则使用这种结构相当笨拙:
实现文件可以导入测试文件 如果没有src
出现在输出文件夹名称中,则无法同时构建test
和src
,您可能不希望这样 仅更改实现文件中的 internals 需要再次类型检查测试,即使这不会导致新的错误 仅更改测试需要再次对实现进行类型检查,即使没有任何更改
您可以使用多个 tsconfig 文件来解决 一些这些问题,但会出现新问题:
没有内置的最新检查,因此您最终总是运行tsc
两次 两次调用tsc
会导致更多的启动时间开销tsc -w
不能同时在多个配置文件上运行
我知道参考资料。但是,据我了解,它们需要构建用于类型检查(如果进行了这种分离,则必须先构建以创建 d.ts 文件),而我宁愿避免这样做。
这种厌恶的原因是什么?
如果是构建一个新的项目克隆的前期成本,那将远远超过构建时间的改进(参见下面的参数)。后者对开发人员生产力的好处将远远超过前者的成本。
具有讽刺意味的是,您对前期成本的关注越大,缩短构建时间所带来的好处就越大!
如果您希望能够在 VS Code 或 WebStorm 等类型和链接感知编辑器中导航新克隆而无需构建,您可以通过将 .d.ts
文件签入源代码控制来实现此目的。
以下是文档的具体说明:
由于依赖项目使用从其依赖项构建的
.d.ts
文件,因此您必须检查某些构建输出或在克隆项目后构建项目,然后才能导航在编辑器中进行项目,而不会看到虚假错误。我们正在开发一个应该能够缓解这种情况的幕后 .d.ts 生成过程,但目前我们建议通知开发人员他们应该在克隆后构建。
项目引用的论据
来自文档:
您可以大大缩短构建时间
期待已久的功能是 TypeScript 项目的智能增量构建。在 3.0 中,您可以将
--build
flag 与tsc
一起使用。这实际上是tsc
的一个新入口点,它的行为更像是一个构建协调器,而不是一个简单的编译器。运行
查找所有引用的项目 检测它们是否是最新的 以正确的顺序构建过时的项目tsc --build
(简称tsc -b
)将执行以下操作:
不用担心对您在命令行中传递的文件进行排序 -
tsc
会在需要时重新排序,以便始终首先构建依赖项。
在组件之间实施逻辑分离
以新的更好的方式组织您的代码。
Project References 文档中有一些更有用的好处/功能。
示例设置
src/tsconfig.json
即使根目录下没有代码,这个 tsconfig 也可以在
所有常见的设置都去了(其他的将从它继承),并且
它将启用一个简单的tsc --build src
来构建整个
项目(并使用 --force
从头开始构建它)。
"compilerOptions":
"rootDir": ".",
"outDir": "../build",
"composite": true
,
// this root project has no source of its own
"files": [],
// but building this project will build all of the following:
"references": [
"path": "./common"
"path": "./projectA"
"path": "./projectB"
]
src/common/tsconfig.json
因为 common 没有引用,所以导入仅限于其目录中的目标和npm_modules
。我相信,你甚至可以通过给它自己的package.json
来限制后者。
"compilerOptions":
"rootDir": ".",
"outDir": "../../build/common",
"composite": true
src/projectA/tsconfig.json
projectA 可以导入 common 因为声明的引用。
"compilerOptions":
"rootDir": ".",
"outDir": "../../build/projectA",
"composite": true
,
"references": [
"path": "../common"
]
src/projectB/tsconfig.json
projectB 可以导入 common AND projectA。
"compilerOptions":
"rootDir": ".",
"outDir": "../../build/projectB",
"composite": true
,
"references": [
"path": "../common"
"path": "../projectA"
]
构建
这些只是一些例子。我使用下面的tsc
开关的缩写形式,例如-b
而不是 --build
。从 repo 根目录执行的所有命令。
tsc -b src
- 构建整个树。
tsc -p src/projectA/
只编译 projectA。
tsc -b src/projectA/
构建 projectA 和任何过期的依赖项。
tsc -b -w src
- 构建并观察整个树。
tsc -b --clean src
- 删除整个树的输出。
tsc -b -f src
- 强制重建整个树。
使用-d
或-dry
开关可以预览tsc -b
的功能。
【讨论】:
谢谢!如果我更改导出参数的类型,我需要运行“tsc -p src/common”来生成 d.ts 文件,并且输入来自 common 的导入正确吗? @BenCarp 是的,完全正确。或者,如果您在处理 projectA 时更改 common,tsc -b src/projectA/
将重新编译更改的 common 项以及 projectA 中受影响的项。在提交您的更改之前,您可以tsc -b src
以确保一切都已构建,或者将其留给您的持续集成系统。我在答案中添加了示例构建命令并修复了示例 tsconfigs。
Inigo,这个具体的项目其实是用Webpack搭建,编译过babel。所以我猜它使构建命令与这个项目无关,我看到有一个 watch 命令。它是否允许对 common 的类型更改自动编译,并对项目产生影响?
tsc --build
命令与您给出的确切原因无关,并且正如我在上面已经解释的“任何过时的依赖项”。设置一个简单的项目来解决您的所有问题很容易。我不知道它如何与 Webpack 和 Babel 一起工作。以上是关于在 Typescript 中防止不适当的导入和执行项目层次结构的主要内容,如果未能解决你的问题,请参考以下文章
如何防止我的 Vue 构建由于 Typescript 错误而失败
如何使用jsdom和typescript防止“属性'...'在'Global'类型上不存在”?