- This is a small reference guide to the elements you can use to write rules for dependency-cruiser. If you want a step-by-step introduction check the rules tutorial.
- There is a json schema that describes the output format. Dependency-cruiser checks rule sets against it.
- Some examples:
- dependency-cruiser's own configuration
- the configuration State Machine cat uses for validation and the one it uses for generating a visual graph.
- mscgen.js's .dependency-cruiser.js
- Tip: run
depcruise --init
to create a .dependency-cruiser.js with some rules that make sense in most projects.
- The structure of a dependency-cruiser configuration
- The structure of an individual rule
- Conditions
path
pathNot
- path specials
orphan
reachable
couldNotResolve
- rules on dependents -
numberOfDependentsLessThan
- rules on dependents -
numberOfDependentsMoreThan
circular
license
andlicenseNot
dependencyTypes
anddependencyTypesNot
dynamic
moreThanOneDependencyType
exoticallyRequired
,exoticRequire
andexoticRequireNot
preCompilationOnly
moreUnstable
- Configurations in JavaScript
The typical dependency-cruiser config is either json file or a JavaScript file
that exports a simple JavaScript object (see Configurations in JavaScript below).
The most important sections in the config are forbidden
, allowed
, required
and options
, so a skeleton config could look something like this:
{
"forbidden": [],
"allowed": [],
"required": [],
"options": {}
}
The following paragraphs explain these as well as other sections.
A list of rules that describe dependencies that are not allowed. dependency-cruiser will emit a separate error (warning/ informational) message for each violated rule.
A list of rules that describe dependencies that are allowed. dependency-cruiser
will emit a 'not-in-allowed' message for each dependency that does not
satisfy at least one of them. The severity of the message is warn by
default, but you can override it with allowedSeverity
The severity to use in reports when a dependency is not in the allowed
list of rules. It takes the same values as other severity
fields and
also defaults to warn
.
A list of rules that describe what dependencies modules must have, like 'every controller must depend on the base controller'.
'Required' rules have slightly different semantics from the forbidden
and
allowed
types. There's a mandatory module
attribute that specifies which
modules the rule applies to and the to
describes what dependencies that module
should exactly have.
To check for direct dependencies (controllers must directly depend on the base controller):
{
required: [
{
name: "controllers-inherit-from-base",
comment:
"Each controller should inherit from the framework's base controller",
module: {
// every module that matches the pattern specified in path & pathNot ...
path: "-controller[.]js$",
pathNot: "framework/base-controller[.]js$",
},
to: {
// ... must depend at least once on the framework's base controller
path: "^src/framework/base-controller[.]js$",
},
},
];
}
To check for indirect ('transitive') dependencies (controllers must depend
on the base controller, either directly or via other modules) add the reachable
attribute to to
part of the rule, like so:
{
required: [
{
name: "controllers-transitively-inherit-from-base",
comment:
"Each controller should inherit from the framework's base controller",
module: {
// every module that matches the pattern specified in path & pathNot ...
path: "-controller[.]js$",
pathNot: "framework/base-controller[.]js$",
},
to: {
// ... must depend at least once on the framework's base controller
path: "^src/framework/base-controller[.]js$",
// ... either directly or indirectly
reachable: true,
},
},
];
}
reachable: false
is the same as leaving the reachable attribute out; so it does 'nothing'.
This takes one or more file path to other dependency-cruiser-configs. When
dependency-cruiser reads your config, it takes the contents of the
extends
and merges them with the contents of your config.
dependency-cruiser resolves the extends
relative to the file name with the
same algorithm node uses, which means a.o.
- names starting with
./
are local - you can use external node_modules to reference rule sets (e.g.
dependency-cruiser/configs/recommended
) - there's no need to specify the extension for JavaScript files, but for json it's mandatory.
allowed
rules
Dependency-cruiser concatenatesallowed
rules from the extends, and de-duplicates them.forbidden
andrequired
rules
Forforbidden
andrequired
rules it uses the same approach, except when the rules have a name, in which case the rule with the same name in the current file gets merged into the one from extends, where attributes from the current file win. This allows you to override only one attribute, e.g. the severityallowedSeverity
If there's anallowedSeverity
in the current file, it wins. If neither file has anallowedSeverity
dependency-cruiser uses warn as a defaultoptions
options
get the Object.assign treatment - where the option in the current file wins.- If there's more than one path in extends, they get merged into the current file one by one, running through the array left to right.
To use a local base config:
{
"extends": "./configs/dependency-cruiser-base.json"
}
To use a base config from an npm package:
{
"extends": "@ourcompany/dependency-cruiser-configs/frontend-rules-base.json"
}
module.exports = {
extends: "dependency-cruiser/configs/recommended",
forbidden: [
{
// because we still use a deprecated core module, still let
// the no-deprecated-core rule from recommended fire,
// but at least temporarily don't let it break our build
// by setting the severity to "warn" here
name: "no-deprecated-core",
severity: "warn",
// no need to specify the from and to, because they're already
// defined in 'recommended'
},
],
};
Options that influence what is cruised, and how it is cruised. See the options reference for an exhaustive list.
A rule consists at least of a from
and a to
attribute that contain one or
more conditions that trigger the rule, so a minimal rule will look like this:
{
"from": {},
"to": {}
}
A rule within the 'allowed' section can also have a comment
attribute
which you can use to describe the rule.
Rules within the 'forbidden' section can have a name
and a severity
.
{
"name": "kebab-cased-name",
"comment": "(optional) description of the rule",
"severity": "warn",
"from": {},
"to": {}
}
Conditions an end of a dependency should match to be caught by this rule. Leave it empty if you want any module to be matched.
The conditions section below describes them all.
You can use this field to document why the rule is there. It's not used in any rule logic.
(only available in the
forbidden
section )
A short name for the rule - will appear in reporters to enable customers to quickly identify a violated rule. Try to keep them short, eslint style. E.g. 'not-to-core' for a rule forbidding dependencies on core modules, or 'not-to-unresolvable' for one that prevents dependencies on modules that probably don't exist.
If you do not provide a name, dependency-cruiser will default it
to unnamed
.
(only available in the
forbidden
section )
How severe a violation of a rule is. The 'error' severity will make
some reporters (at least the err
one) return a non-zero exit
code, so if you want e.g. a build to stop when there's a rule
violated: use that.
The other values you can use are info
, warn
and ignore
. If you
leave it out dependency-cruiser will assume it to be warn
.
With the severity set to ignore
dependency-cruiser will not check
the rule at all. This can be useful if you want to temporarily
disable a rule or disable a rule you inherited from a rule set you
extended.
If you want to set the severity for the 'allowed' rules you can use
allowedSeverity
What to apply the rule to - either module
(the default) or folder
. For many
rules this makes no difference. For these rules we advise to not include the
scope
attribute at all. There are two notable exceptions where it does make
a difference:
- rules regarding Instability metrics A folder can contain a highly instable module (e.g. an index.js barrel that depends on everything and their mother within the module), but be stable itself e.g. because there's nothing in the folder that depends on modules outside it.
- rules regarding circular dependencies
For example, when
main/index.js
depends onutl/helper.js
andutl/helper.js
depends onmain/utl.js
there is no circular dependency on module level, but there is one between themain
andutl
folders
Note
at this time only the moreUnstable
, circular
and path
/ pathNot
attributes (including 'group matching') work, so it is
possible to check the "stable dependencies principle" on folder level. Other
attributes (including, but not limited to via)
still have to be implemented (after release 11.7.0)
A regular expression an end of a dependency should match to be caught by this rule.
In from
, this is the path from the current working directory (typically your
project root) to the file containing a dependency. In to
, this is the path from
the current working directory to the file the dependency resolves to.
When path is in a to
part of a rule it accepts the regular expression
'group matching' special variables $0
, $1
, $2
, ... as well. See
'group matching' below for an explanation & example.
A regular expression an end of a dependency should NOT match to be caught by this rule.
When pathNot is in a to
part of a rule it accepts the regular expression
'group matching' special variables $0
, $1
, $2
, ... just like the path
attribute. See 'group matching' below for an explanation & example.
I chose regular expressions for matching paths over the more traditional glob because they're more expressive - which makes it easier to specify rules. Some common patterns
glob | regular expression | this expresses: |
---|---|---|
*.js |
[^/]+\.js$ |
files in the current folder with the extension .js |
src/**/* |
^src |
all files in the src folder |
not possible | ^src/([^/]+)/.+ |
everything in the src tree - remember the matched folder name directly under src for later reference. |
To make sure rules you specify run on all platforms, dependency-cruiser
internally represents paths with forward slashes as path separators
(src/alez/houpe
).
Sometimes you'll want to use a part of the path the 'from' part of your rule matched and use it in the 'to' part. E.g. when you want to prevent stuff in the same folder to be matched.
To achieve this you'll need to do two things:
- In the
from
of your rule:
Make sure the part of thepath
you want to be matched is between brackets. Like so:"^src/([^/]+)/.+"
- In the
to
part of your rule:
You can reference the part matched between brackets by using$1
inpath
andpathNot
rules. Like so:"pathNot": "^src/$1/.+".
- It is possible to use more than one group per rule as well. E.g. this
expression
"^src/([^/]+)/[^\.]\.(.+)$"
has two groups; one for the folder directly under src, and one for the extension. The first is available in theto
part of your rule with$1
, the second with$2
. - The special variable
$0
contains the whole matched string. I haven't seen a practical use for it in the context of dependency-cruiser, but I'll be glad to be surprised.
Say you have the following folder structure
src
└── business-components
├── search
├── upsell
├── check-out
├── view-trip
└── check-in
Business components should be completely independent of each other. So typically you'd specify a rule like this to prevent accidents in the "forbidden" section:
{
"name": "no-inter-ubc",
"comment": "Don't allow relations between code in business components",
"severity": "error",
"from": { "path": "^src/business-components/([^/]+)/.+" },
"to": {
"path": "^src/business-components/([^/]+)/.+"
}
}
This will correctly flag relations from one folder to another, but also
relations within folders. It's possible to get around that by specifying it
for each folder explicitly, leaving the current 'from' folder from the to
list e.g.
from: search, to: upsell|check-out|view-trip|check-in,
from: upsell, to: search|check-out|view-trip|check-in,
...
That'll be heavy maintenance though; especially when your business components breed like a litter of rabbits. In stead, you can use group matching:
{
"name": "no-inter-ubc",
"comment": "Don't allow relations between business components",
"severity": "error",
"from": { "path": "^src/business-components/([^/]+)/.+" },
"to": {
"path": "^src/business-components/([^/]+)/.+",
"pathNot": "^src/business-components/$1/.+"
}
}
... which makes sure dependency-cruiser does not match stuff in the from folder currently being matched.
When your regular expressions grow bigger, you might want to express them as an array of regular expressions instead of one regular expression to improve legibility.
example ...
{
from: {
path: "(^src/report/)"
},
to: {
pathNot: "$1|^node_modules|^(path|package.json)$|^src/utl"
}
}
{
from: {
path: "(^src/report/)"
},
to: {
pathNot: [
"$1",
"^node_modules",
"^(path|package.json)$",
"^src/utl"
]
}
}
A Boolean indicating whether or not to match modules that have no incoming
and no outgoing dependencies. Orphans might need special attention because
they're unused leftovers from a refactoring. Or the start of some feature
that never got finished but which was merged anyway. Leaving the orphan
attribute out means you don't care about orphans in your code.
Detecting orphans will have an impact on performance. You will probably only notice it when you have a larger code base (thousands of modules in your dependency graph), but it is something to keep in mind.
To detect orphan guys you can add e.g. this snippet to your
.dependency-cruiser.js's forbidden
section:
{
"name": "no-orphans",
"severity": "warn",
"from": { "orphan": true },
"to": {}
}
- 💡 dependency-cruiser will typically not find orphans when you give it
only one module to start with. Any module it finds, it finds by
following its dependencies, so each module will have at least one
dependency incoming or outgoing. Specify one or more folder, several
files or a glob. E.g.
will find orphans if they exist, whereas
depcruise -v -- src lib test
probably won't (unless index.ts is an orphan itself).depcruise -v -- src/index.ts
- 💡 by definition orphan modules have no dependencies. So when
orphan
is part of a rule, theto
part won't make sense. This is why dependency-cruiser will ignore theto
part of these rules. - 💡 For similar reasons
orphan
is not allowed in theto
part of rules.
reachable
is a Boolean indicating whether or not modules matching the to
part
of the rule are reachable (either directly or via other modules) from modules
matching the from
part of the rule. This can be useful for two use cases:
For instance, in this dependency-graph several modules are not reachable from
the root index.js
. If index.js
is the only (legal) entry to this package,
those unreachable modules are likely candidates for removal:
Here's a rule snippet that will detect these for you:
{
"forbidden": [
{
"name": "no-unreachable-from-root",
"severity": "error",
"from": {
"path": "src/index\\.js$"
},
"to": {
"path": "src",
/*
spec files shouldn't be reachable from regular code anyway, so you
might typically want to exclude these from reachability rules.
The same goes for typescript definition files:
*/
"pathNot": "\\.spec\\.(js|ts)$|\\.d\\.ts$"
/*
for each file matching path and pathNot, check if it's reachable from the
modules matching the criteria mentioned in "from"
*/
"reachable": false
}
}
]
}
With this rule enabled, the unreachable rules jump out immediately. Both in the output of the err
reporter
error no-unreachable-from-root: src/other-stuff/index.js
error no-unreachable-from-root: src/other-stuff/untouched-one.js
error no-unreachable-from-root: src/other-stuff/untouched-two.js
error no-unreachable-from-root: src/relevant/to-untouched.js
✖ 4 dependency violations (4 errors, 0 warnings). 8 modules cruised.
... and in the output of the dot
one:
You can use the same reachable
attribute to find transient dependencies (fancy
way to say via via). Let's say you have a bunch of JavaScript files that define
static schemas. It's OK if they import stuff, but they should never touch
database implementation code (which happens to live in src/lib/database
).
With a rule like this in the forbidden
section you can make sure that never
happens:
{
"name": "implementation-not-reachable-from-info-ts",
"comment": "Don't allow importing database implementation files for schema declaration files",
"severity": "error",
"from": {
"path": "\\.schema\\.ts$"
},
"to": {
"path": "^src/libs/database/",
"reachable": true
}
}
- 💡 You can set up multiple rules with a
reachable
attribute in theto
section. If you do so, make sure you give aname
to each rule. It's not only the only way dependency-cruiser can keep reachable rules apart - it will be for you as well :-). - 💡 Different from other rules, rules with a
reachable
attribute can only havepath
andpathNot
in thefrom
part of the rulepath
andpathNot
alongside thereachable
in theto
part of the rule
(these limitations might get lifted somewhere in the future)
Matches when the number of dependents of a module is less than the provided number. Useful to detect whether modules are 'shared' enough to your liking, or whether they're actually used in the first place.
E.g. to flag modules in the shared
folder that are only used from the
features
folder once (or not a all), you can use a rule like this in the
forbidden
section
{
name: "no-unshared-in-shared",
from: {
path: "^features/"
},
module: {
path: "^shared/",
numberOfDependentsLessThan: 2
}
}
-
💡 Currently rules on dependents only work within the
forbidden
context. -
💡 In the
from
partpath
andpathNot
attributes work, but none other. -
💡 Similarly the
to
part of the rule can (next to thenumberOfDependentsLessThan
attribute) also only usepath
andpathNot
. -
💡 You can't use group matching with this rule.
why?
Unlike regular dependency rules this rule will not match one module at a time, but a whole bunch of them. With one match (i.e.
src/coolstuff/ice.ts
and a a regular expression with a group (/^src/(^[/]+)/
) the$1
variable can only mean one thing (coolstuff
). With more than one result (src/coolstuff/ice.ts, src/hotstuff/pepper.ts
) there's also more than one thing$1
means - making use in e.g.to.path
ambiguous.
Matches when the number of dependents of a module is more than the provided number. Useful to
- detect whether modules are 'shared' enough to your liking, or whether they're actually used in the first place, but you want or need to enforce that in an allowed rule
- put a maximum number on the number of dependents a module can have (not sure it's like super useful or anything, it's just a side-effect of the previous use)
E.g. to flag modules in the shared
folder that are only used from the
features
folder once (or not a all), you can use a rule like this in the
allowed
section
{
name: "no-unshared-in-shared",
from: {
path: "^features/"
},
module: {
path: "^shared/",
numberOfDependentsMoreThan: 1
}
}
The same usage notes and caveats apply as do for the numberOfDependentsLessThan attribute.
Whether or not to match modules dependency-cruiser could not resolve (and probably aren't on disk). For this one too: leave out if you don't care either way.
To get an error for each unresolvable dependency, put this in your "forbidden" section:
{
"name": "not-to-unresolvable",
"severity": "error",
"from": {},
"to": { "couldNotResolve": true }
}
A Boolean indicating whether or not to match module dependencies that end up where you started (a.k.a. circular dependencies). Leaving this out => you don't care either way.
For example, adding this rule to the "forbidden" section in your .dependency-cruiser.js will issue a warning for each dependency that ends up at itself.
{
"name": "no-circular",
"severity": "warn",
"from": { "pathNot": "^(node_modules)" },
"to": { "circular": true }
}
There are two attributes to put restrictions on through which modules cycles are allowed to pass:
via
: matches against some of the modules in the cycleviaOnly
: matches against all of the modules in the cycle
Both take the path
, pathNot
and dependencyTypes
, dependencyTypesNot
attributes that have a meaning similar to the ones in the from
and to
parts of a rule, with dependencyTypes expressing the type of relation a
module has with its predecessor in the cycle.
With the cycle a/aa.js
-> a/ab.js
-> b/bb.js
-> a/aa.js
the restrictions
to this:
restriction | what it does | example input | match? | because... |
---|---|---|---|---|
via |
some of the modules in the cycle match the condition | path: "^a/.+" |
true |
a/aa.js and a/ab.js match |
viaOnly |
all of the modules in the cycle match the condition | path: "^a/.+" |
false |
b/bb.js doesn't match |
viaNot |
deprecated - use viaOnly.pathNot in stead |
path: "^a/.+" |
false |
a/aa.js and a/ab.js match |
viaSomeNot |
deprecated - use via.pathNot in stead |
path: "^a/.+" |
true |
b/bb.js doesn't match |
If you're only interested in cycles at run time, you might allow dependencies
that are only used at compile time to be part of a cycle. type-only
ones
are a good example of that. E.g. at run time this cycle, where r.ts
imports
some things from s.ts
as 'type-only':
flowchart LR
p.ts --> q.ts --> r.ts --> |type-only| s.ts --> p.ts
... would look like this at run time (so: not a cycle):
flowchart LR
p.ts --> q.ts --> r.ts
s.ts --> p.ts
To express this in a rule, you can use the viaOnly
attribute:
// log an error for all circular dependencies which consist of only non-`type-only`
// dependencies
{
name: 'no-circular-at-runtime',
severity: 'error',
from: {
},
to: {
circular: true,
viaOnly: {
dependencyTypesNot: ['type-only']
}
}
},
Some codebases include a lot of circular dependencies, sometimes with a few 'knots' (typically barrel files) that partake in most of them. Fixing these cycles might take a spell, so you might want to (temporarily :-) ) exclude them from breaking the build. At the same time you might want to prevent any new violation going unnoticed because of this.
The recommended solution for this is dependency-cruiser's
ignore-known
mechanism.
However, you can also use the via
attribute.
In this example app/javascript/tables/index.ts
and app/javascript/tables/index.ts
are the known 'knots':
// log an error for all circular dependencies except when they are via one of the
// known 'knots'
{
name: 'no-circular',
severity: 'error',
from: {
},
to: {
circular: true,
via: {
pathNot: [
'^app/javascript/tables/index.ts',
'^app/javascript/ui/index.tsx',
]
}
}
},
// for circular dependencies that pass through one of the knots, still generate
// a warning
{
name: 'no-circular (exception for known barrels)',
severity: 'warn',
from: {
},
to: {
circular: true,
via: {
path: [
'^app/javascript/tables/index.ts',
'^app/javascript/ui/index.tsx',
]
}
}
}
This example (adapted from a
question on GitHub
by @PetFeld-ed) not only uses the via
, but
also displays group matching.
// in the `forbidden` section of a dependency-cruiser config:
{
name: 'no-circular-dependency-of-modules',
comment:
'If a module in business component A depends on one in component B, then ' +
'the module in component B should not depend on that component in module A ' +
'This is also forbidden if the dependency is transitive. ',
"I.a.w.: if there's a cycle, it should stay within the same component " +
severity: 'error',
from: {
path: '^src/business-components/([^/]+)/.+'
},
to: {
circular: true,
via: {
pathNot: '^src/business-components/$1/.+'
},
// previously written with the now deprecated viaSomeNot attribute:
// viaSomeNot: '^src/business-components/$1/.+'
},
}
You can flag dependent modules that have licenses that are e.g. not
compatible with your own license or with the policies within your company with
license
and licenseNot
. Both take a regular expression that matches
against the license string that goes with the dependency.
E.g. to forbid GPL and APL licenses (which require you to publish your source code - which will not always be what you want):
{
"name": "no-gpl-apl-licenses",
"severity": "error",
"from": {},
"to": { "license": "GPL|APL" }
}
This raise an error when you use a dependency that has a string with GPL or
APL in the "license" attribute of its package.json (e.g.
SPDX compatible expressions like GPL-3.0
, APL-1.0
and
MIT OR GPL-3.0
but also on non SPDX compatible)
To only allow licenses from an approved list (e.g. a greenlist provided by your legal department):
{
"name": "only-licenses-approved-by-legal",
"severity": "warn",
"from": {},
"to": { "licenseNot": ["MIT", " ISC"] }
}
Just with path and pathNot you can pass an array of regular expressions as well if you think that's more legible.
Note
dependency-cruiser can help out a bit here, but you remain responsible for managing your own legal stuff. To re-iterate what is in the LICENSE to dependency-cruiser:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
You might have spent some time wondering why something works on your machine, but not on other's. Only to discover you did install a dependency, but did not save it to package.json. Or you already had it in your devDependencies and started using it in a production source.
To save you from embarrassing moments like this, you can make rules with the
dependencyTypes
restriction. E.g. to prevent you accidentally depend on a
devDependency
from anything in src
add this to your
.dependency-cruiser.js's "forbidden" section:
{
"name": "not-to-dev-dep",
"severity": "error",
"comment": "because an npm i --production will otherwise deliver an unreliably running package",
"from": { "path": "^src" },
"to": { "dependencyTypes": ["npm-dev"] }
}
Or to detect stuff you npm i'd without putting it in your package.json:
{
"name": "no-non-package-json",
"severity": "error",
"comment": "because an npm i --production will otherwise deliver an unreliably running package",
"from": { "pathNot": "^(node_modules)" },
"to": {
"dependencyTypes": ["unknown", "undetermined", "npm-no-pkg", "npm-unknown"]
}
}
Likewise you can use the inverse dependencyTypesNot
restriction. E.g. to ensure
type-only imports (e.g. import type { IYadda } from "./types"
) are used from
.d.ts
modules and/ or modules called types.ts
:
{
"name": "only-type-only",
"comment": "use explicit 'type' imports to import from type declaration modules",
"severity": "error",
"from": {},
"to": {
"path": ["types\\.ts$", "\\.d\\.ts$"],
"dependencyTypesNot": ["type-only"]
}
}
If you don't specify dependencyTypes (or dependencyTypesNot) in a rule, dependency-cruiser will ignore them in the evaluation of that rule.
This is a list of the main dependency types dependency-cruiser currently detects.
dependency type | meaning | example |
---|---|---|
local | a module in your own ('local') package | "./klont" |
localmodule | a module in your own ('local') package, but which was in the resolve.modules attribute of the webpack config you passed |
"shared/stuff.ts" |
npm | it's a module in package.json's dependencies |
"lodash" |
npm-dev | it's a module in package.json's devDependencies |
"chai" |
npm-optional | it's a module in package.json's optionalDependencies |
"livescript" |
npm-peer | it's a module in package.json's peerDependencies - note: deprecated in npm 3, but later on revived. |
"thing-i-am-a-plugin-for" |
npm-bundled | it's a module that occurs in package.json's bundle(d)Dependencies array |
"iwillgetbundled" |
npm-no-pkg | it's an npm module - but it's nowhere in your package.json | "forgetmenot" |
npm-unknown | it's an npm module - but there is no (parseable/ valid) package.json in your package | |
deprecated | it's an npm module, but the version you're using or the module itself is officially deprecated | "some-deprecated-package" |
core | it's a (nodejs) core module. B.t.w. you can influence this | "fs", "node:test" |
unknown | it's unknown what kind of dependency type this is - probably because the module could not be resolved in the first place | "loodash" |
undetermined | the dependency fell through all detection holes. | "veloci!./raptor" |
These are types that will typically occur alongside the ones above, telling a little bit more on how
the dependency was declared. One or more of these can occur at the same time. E.g. if you import a
dependency which resolves to a base url in a tsconfig.json you'll see import
, aliased
as well as
aliased-tsconfig
and aliased-tsconfig-base-url
.
dependency type | meaning the module was imported ... | example |
---|---|---|
aliased | via an alias of some sort (e.g. tsconfig paths, subpath imports, npm workspace or webpack aliases) | "~/hello.ts" |
aliased-subpath-import | via a subpath import | "#thing/hello.mjs" |
aliased-tsconfig | via a typescript compilerOptions.paths or compilerOptions.baseUrl setting in tsconfig. | "@thing/hello" |
aliased-tsconfig-base-url | via a typescript compilerOptions.baseUrl setting in tsconfig | "libs/utensils/src/hello.js" |
aliased-tsconfig-paths | via a typescript compilerOptions.paths setting in tsconfig | "@thing/hello" |
aliased-webpack | via a webpack resolve alias | "Utilities" |
aliased-workspace | via a workspace | "local-workspace-package" |
amd-define | with an AMD define wrapper (popularized by requirejs) |
define(["./thing"], function(thing){ /* do stuff */ }) |
amd-exotic-require | with a require statement within an AMD module (but with the first parameter baring an insensible non-standard name) | define(function(want, exports, module){ var one = want('./thing')}) |
amd-require | with a require statement within an AMD module | define(function(require, exports, module){ var one = require('./thing')}) |
dynamic-import | with a dynamic import statement | const { thing } = await import("./things") |
exotic-require | with a statement that isn't 'require' see exoticallyRequired | const { thing } = want("./thing") |
export | implicitly via a module export | export { thing } from "./things" |
import | with a 'regular' ES import | import { thing } from "./things |
import-equals | with an 'import equals' statement | import fs = require("fs") |
jsdoc | in jsdoc. See jsdoc-bracket-import & jsdoc-import-tag . Needs detectJSDocImports set to true |
/** @type {import('./things').thing} */ , /** @import { thing } from "things" */ |
jsdoc-bracket-import | in jsdoc with a 'bracket' style import. Always type-only , also has the jsdoc dependency type. |
/** @type {import('./things').thing} */ |
jsdoc-import-tag | in jsdoc with an @import tag. Always type-only , also has the jsdoc dependency type. |
/** @import { thing } from "things" */ |
pre-compilation-only | but the dependency will disappear at runtime. See preCompilationOnly | import { thing } from "./things" // and continue to not use thing |
require | with a commonjs 'require' statement | const memoize = require("lodash/memoize") |
triple-slash-amd-dependency | with a triple slash directive, specifically declaring an AMD dependency | /// <amd-dependency path="./ts-thing-types" /> |
triple-slash-directive | with a triple slash directive (oldskool TypeScript) | |
triple-slash-file-reference | with a triple slash directive, specifically importing another module | /// <reference path="./ts-thing" /> |
triple-slash-type-reference | with a triple slash directive, specifically importing types | /// <reference types="./ts-thing-types" /> |
type-import | as part of a type declaration | const lAThing: import('./things').IThing = {} |
type-only | as 'type only' - only available for TypeScript sources, only for tsPreCompilationDeps !== false. | import type { IThing } from "./things" |
A Boolean that tells you whether the dependency is a dynamic one (i.e.
it uses the async ES import statement a la import('othermodule').then(pMod => pMod.doStuff())
).
You can use this e.g. to restrict the usage of dynamic dependencies:
{
"forbidden": [
{
"name": "no-non-dynamic-dependencies",
"severity": "error",
"from": {},
"to": { "dynamic": true }
}
]
}
... or to enforce the use of dynamic dependencies for certain dependencies
{
"forbidden": [
{
"name": "only-dyn-deps-to-otherside",
"comment": "only dynamically depend on 'otherside' modules",
"severity": "error",
"from": {},
"to": { "path": "@theotherside/", "dynamic": false }
}
]
}
With the flexible character of package.json it's totally possible to specify
a package more than once - e.g. both in the peerDependencies
and in the
dependencies
. Sometimes this is intentional (e.g. to make sure a plugin
type package works with both npm 2 and 3), but it can be a typo as well.
Anyway, it's useful to be conscious about it. You can check
for it with a moreThanOneDependencyType
attribute - which matches these
when set to true:
{
"name": "no-duplicate-dep-types",
"severity": "warn",
"from": {},
"to": { "moreThanOneDependencyType": true }
}
For this rule the aliased
, aliased-subpath-import
, aliased-webpack
,
aliased-tsconfig
and type-only
dependency types do not count towards the
number of dependency types a dependency has, as these by definition are
always in addition to one of the other dependency types.
When left out it doesn't matter how many dependency types a dependency has.
(If you're more of an 'allowed' user: it matches the 0 and 1 cases when set to false).
For exotic requires/ require wrappers you might want to have different rules a.c.t. normal requires. E.g.
- When you use a require wrapper to include a dependency that might not be there and handle it elegantly, it's not an error if the module-to-be-there doesn't actually exist - or is e.g. in your optionalDependencies.
- You might want to only allow the use of certain dependencies through an exotic require:
{
"name": "not-to-optional-deps",
"severity": "error",
"from": {},
"to": {
"dependencyTypes": ["npm-optional"],
"exoticallyRequired": false
}
}
- ... or ban exotic requires altogether:
{
"name": "ban-all-exotic-requires",
"severity": "error",
"from": {},
"to": {
"exoticallyRequired": true
}
}
- Or allow only one specific:
{
"name": "only-window-require-exotic",
"severity": "error",
"comment": "The only 'exotic' require allowed is window.require",
"from": {},
"to": {
"exoticRequireNot": "^window\\.require$",
"exoticallyRequired": true
}
}
If you want to set restrictions on dependencies that only exist before
compilation from TypeScript to JavaScript, you can use the (boolean)
preCompilation
only attribute.
E.g. to make sure to only import stuff from the react-native stuff that doesn't make it beyond the pre-compilation step:
{
"forbidden": [
{
"name": "only-types-from-react-native",
"description": "make sure to only import stuff that's portable over platforms",
"severity": "error",
"from": {
"path": "^src/platform-independent-stuff"
},
"to": {
"path": "^src/lib/react-native-stuff",
"preCompilationOnly": false
}
}
],
"options": {
"tsPreCompilationDeps": "specify"
}
}
Note
This attribute only works for TypeScript sources, and only when
tsPreCompilationDeps
option is set to "specify"
.
When set to true moreUnstable matches for any dependency that has a higher Instability than the module that depends on it. When set to false it matches when the opposite is true; the dependency has an equal or lower Instability.
This attribute is useful when you want to check against Robert C. Martin's
stable dependencies principle: "depend in the direction of stability". Martin
defines
E.g. a module with no dependencies and one or more dependents has a Instability of 0%; as stable as it can get. Conversely a module with no dependents, but with one or more dependencies is 100% Instable.
Instability has a bit of an unusual connotation here - it's not 'bad' to be a 100% Instable module - it's only the nature of the module. A CLI or GUI component typically only depends on other modules and is 100% Instable. This is not a bad thing.
Another way to look at Instability is how hard it is to change a module without consequences to other modules. Changing a 0% Instable module will typically have consequences for all its dependents. Making changes to a 100% Instable module will have consequences for itself only.
To enforce/ find all modules that violate the stable dependencies principle (SDP) you can include a rule like this in the forbidden section
module.exports = {
forbidden: [
{
name: "SDP",
comment:
"This module violates the 'stable dependencies' principle; it depends " +
"on a module that is likely to be more prone to changes than it is " +
"itself. Consider refactoring.",
from: {},
to: {
moreUnstable: true,
},
},
],
};
A note on using metrics:
..., a metric is not a god; it is merely a measurement against an arbitrary standard. It is certainly possible that the standard chosen in this chapter is appropriate only for certain applications and not for others.
Robert C. Martin - Agile Principles, Patterns and Practices in C# (2006)
From version 4.7.0 you can pass a JavaScript module to --validate
.
It'll work as long as it exports a valid configuration object
and node can understand it.
This allows you to do all sorts of nifty stuff, like composing rule sets or using function predicates in rules. For example:
const subNotAllowed = require("rules/sub-not-allowed.json");
const noInterComponents = require("rules/sub-no-inter-components.json");
module.exports = {
forbidden: [subNotAllowed, noInterComponents],
options: {
tsConfig: {
fileName: "./tsconfig.json",
},
},
};
If you're looking for configuring the
dot
,archi
orddot
reporters, that section has moved to the options reference.