问题
In a TS project I'd like the following to be blocked:
- A file from folder common importing from folder projectA
- A file from folder projectB importing from folder projectA
I'd like the following to be allowed:
- A file from folder projectA importing from folder projectA
- A file from folder projectA importing from folder common.
I'm aware of References. However, as I understand, they require a build for type checking (If such a separation is made, one must build to create d.ts files first) which I'd rather avoid.
What options do I have? Is it possible to achieve simply via separate tsconfig files for each of those projects/folders?
回答1:
TLDR; You really should just use References. It is exactly what they are for.
But let's address some of your specific thoughts first:
Is it possible to achieve simply via separate tsconfig files for each of those projects/folders?
Yes, but it's not very flexible. You could isolate common by setting its
rootDir
to.
. You would then get an'/path/to/projectA' is not under 'rootDir'
error if you tried to import projectA into common. But to be able to import common into projectA, itsrootDir
would have to be more global, but then that would allow you to import projectB.Not only that, according to the Project References documentation:
Previously, this structure was rather awkward to work with if you used a single tsconfig file:
- It was possible for the implementation files to import the test files
- It wasn’t possible to build
test
andsrc
at the same time without havingsrc
appear in the output folder name, which you probably don’t want - Changing just the internals in the implementation files required typechecking the tests again, even though this wouldn’t ever cause new errors
- Changing just the tests required typechecking the implementation again, even if nothing changed
You could use multiple tsconfig files to solve some of those problems, but new ones would appear:
- There’s no built-in up-to-date checking, so you end up always running
tsc
twice - Invoking
tsc
twice incurs more startup time overhead tsc -w
can’t run on multiple config files at once
I'm aware of References. However, as I understand, they require a build for type checking (If such a separation is made, one must build to create d.ts files first) which I'd rather avoid.
What's the reason for this aversion?
If it's the upfront cost of building a fresh project clone, that will be more than made up for by the improved build times (see arguments for below). The benefits of the latter for developer productivity will far outweigh the costs of the former.
Ironically, the larger you concern about the upfront cost, the larger the benefit from the improved build times!
If it's that you want to be able to navigate a fresh clone in a type and linkage aware editor like VS Code or WebStorm without having to build, you can achieve this by checking the
.d.ts
files into source control.
Here's what the docs say specifically:
Because dependent projects make use of
.d.ts
files that are built from their dependencies, you’ll either have to check in certain build outputs or build a project after cloning it before you can navigate the project in an editor without seeing spurious errors. We’re working on a behind-the-scenes .d.ts generation process that should be able to mitigate this, but for now we recommend informing developers that they should build after cloning.
The argument for Project References
From the docs:
you can greatly improve build times
A long-awaited feature is smart incremental builds for TypeScript projects. In 3.0 you can use the
--build
flag withtsc
. This is effectively a new entry point fortsc
that behaves more like a build orchestrator than a simple compiler.Running
tsc --build
(tsc -b
for short) will do the following:- Find all referenced projects
- Detect if they are up-to-date
- Build out-of-date projects in the correct order
Don’t worry about ordering the files you pass on the commandline -
tsc
will re-order them if needed so that dependencies are always built first.enforce logical separation between components
organize your code in new and better ways.
There's some more useful benefits / features in the Project References doc.
Example setup
src/tsconfig.json
Even if you have no code at the root, this tsconfig can be where all the common settings go (the others will inherit from it), and it will enable a simple
tsc --build src
to build the whole project (and with--force
to build it from scratch).{ "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
Because common has no references, imports are limited to targets within its directory and
npm_modules
. You could even restrict the latter, I believe, by giving it its ownpackage.json
.{ "compilerOptions": { "rootDir": ".", "outDir": "../../build/common", "composite": true } }
src/projectA/tsconfig.json
projectA can import common because of the declared reference.
{ "compilerOptions": { "rootDir": ".", "outDir": "../../build/projectA", "composite": true }, "references": [ { "path": "../common" } ] }
src/projectB/tsconfig.json
projectB can import common AND projectA because of the declared references.
{ "compilerOptions": { "rootDir": ".", "outDir": "../../build/projectB", "composite": true }, "references": [ { "path": "../common" } { "path": "../projectA" } ] }
Builds
These are just some examples. I use the abbreviate forms of tsc
switches below, e.g. -b
instead of --build
. All commands executed from the repo root.
tsc -b src
- builds the entire tree.
tsc -p src/projectA/
compiles just projectA.
tsc -b src/projectA/
builds projectA and any dependencies that are out of date.
tsc -b -w src
- build & watch the entire tree.
tsc -b --clean src
- delete the output for the entire tree.
tsc -b -f src
- force a rebuild of the entire tree.
Use the -d
or -dry
switch to get a preview of what tsc -b
will do.
回答2:
I suggest to use a linter for that job, no need to adjust the build step or use Project References.
eslint-plugin-import is a quite popular ESLint plugin, compatible to TS and can do what you want. After having configured typescript-eslint (if not already done), you can play around with these rules:
- import/no-restricted-paths - Restrict which files can be imported in a given folder
- import/no-relative-parent-imports - Prevent imports to folders in relative parent paths
- import/no-internal-modules - Prevent importing the submodules of other modules
Let's try with following project structure:
| .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",
},
};
Each zone consists of the target path and a from path. The target is the path where the restricted imports should be applied. The from path defines the folder that is not allowed to be used in an import.
Looking into file ./src/common/common.ts
:
import { a } from "../projectA/a"; // works
// Error: Unexpected path "../projectB/b" imported in restricted zone.
import { b } from "../projectB/b";
The import/no-relative-parent-imports
rule also complains for both imports, like for a.ts
:
Relative imports from parent directories are not allowed. Please either pass what you're importing through at runtime (dependency injection), move
common.ts
to same directory as../projectA/a
or consider making../projectA/a
a package.
The third rule import/no-internal-modules
wasn't used, but I also list it here, as it can be very useful to restrict access to child folders/modules and emulate (at least) some kind of package internal modifier in TS.
来源:https://stackoverflow.com/questions/61241866/preventing-inappropriate-imports-and-enforcing-project-hierarchy-in-typescript