Skip to content

Commit 4a9b175

Browse files
authored
feat: Add dependency-cruiser for dependency graph validation (#2999)
* feat(boilerplate): add dependency-cruiser configuration and update navigation types - Introduced a new `.dependency-cruiser.js` file for managing dependency rules and warnings. - Updated `package.json` to include a script for running dependency-cruiser. - Refactored navigation type definitions by consolidating them into `navigationTypes.ts` for better organization. - Adjusted imports across various components to utilize the new navigation types structure. * feat(cli): add dependency-cruiser configuration and update package scripts - Introduced a new `.dependency-cruiser.js` file to manage dependency rules and warnings. - Updated `package.json` to include a script for running dependency-cruiser. - Modified CircleCI configuration to run dependency-cruiser as part of the static tests. - Refactored generator options in `src/tools/generators.ts` for improved type handling. - Updated `src/tools/spawn.ts` to use `cross-spawn` directly for better clarity. * feat(cli): add scripts for generating dependency graph visualizations - Updated `.gitignore` to exclude generated dependency graph files. - Enhanced `package.json` with a new script to generate SVG and PNG visualizations of the dependency graph using dependency-cruiser. * fix(cli): update dependency graph script to use 'src' instead of 'app' - Modified the `depcruise:graph` script in `package.json` to target the 'src' directory for generating dependency graph visualizations. - Updated `.gitignore` to include new graph output files. * fix(boilerplate): update ReactotronConfig import path and clean demo dependencies - Adjusted the import path for ReactotronConfig in _layout.tsx to reflect the correct directory structure. - Removed "@react-navigation/bottom-tabs" from demoDependenciesToRemove in demo.ts for cleaner dependency management. * refactor(boilerplate): move hasValidStringProp utility to DemoShowroomScreen - Integrated the hasValidStringProp function directly into DemoShowroomScreen.tsx for improved accessibility and type safety. - Removed the standalone hasValidStringProp.ts file to streamline the codebase. * fix(boilerplate): correct ReactotronConfig import path in _layout.tsx - Updated the import path for ReactotronConfig to align with the new directory structure. * feat(boilerplate): add utility function for delaying execution in dependency-cruiser config - Included "utils/delay.ts" in the dependency-cruiser configuration to manage execution delays effectively. * chore(boilerplate): update .gitignore to exclude app-dependency-graph.dot file * feat(boilerplate): update dependency-cruiser configuration to use metroConfig for extensions - Replaced hardcoded file extensions in .dependency-cruiser.js with dynamic mapping from metroConfig's sourceExts for improved flexibility and maintainability. * fix(boilerplate): update ReactotronConfig import path to use alias - Changed the import path for ReactotronConfig in _layout.tsx to use the alias "@/devtools/ReactotronConfig" for consistency with the project's directory structure. * refactor(boilerplate): enhance dependency-cruiser configuration with dynamic platform extensions - Added support for multiple platform-specific file extensions in .dependency-cruiser.js by dynamically mapping them from metroConfig's sourceExts, improving flexibility and maintainability. * feat(cli): implement update function for package.json in Expo Router - Added a new utility function to update the `package.json` scripts for dependency-cruiser to use the 'src' directory instead of 'app', enhancing project structure consistency. - Removed outdated script replacements from the previous implementation for cleaner code.
1 parent 62341b2 commit 4a9b175

40 files changed

Lines changed: 791 additions & 106 deletions

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ jobs:
5959
key: ignite-deps-cache-{{ .Environment.IGNITE_DEPS_PACKAGER }}-{{ checksum "boilerplate/package.json" }}-{{ arch }}-{{ .Environment.IGNITE_DEPS_KEY_SUFFIX }}
6060
- run:
6161
name: Run static tests
62-
command: yarn format:check && yarn lint && yarn typecheck
62+
command: yarn format:check && yarn lint && yarn typecheck && yarn depcruise
6363
- run:
6464
name: Run jest tests
6565
command: yarn test

.dependency-cruiser.js

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/** @type {import('dependency-cruiser').IConfiguration} */
2+
module.exports = {
3+
forbidden: [
4+
{
5+
name: "no-circular",
6+
severity: "warn",
7+
comment:
8+
"This dependency is part of a circular relationship. You might want to revise " +
9+
"your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ",
10+
from: {},
11+
to: {
12+
circular: true,
13+
},
14+
},
15+
{
16+
name: "no-orphans",
17+
comment:
18+
"This is an orphan module - it's likely not used (anymore?). Either use it or " +
19+
"remove it. If it's logical this module is an orphan (i.e. it's a config file), " +
20+
"add an exception for it in your dependency-cruiser configuration. By default " +
21+
"this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " +
22+
"files (.d.ts), tsconfig.json and some of the babel and webpack configs.",
23+
severity: "warn",
24+
from: {
25+
orphan: true,
26+
pathNot: [
27+
"(^|/)\\.[^/]+\\.(js|cjs|mjs|ts|json)$",
28+
"\\.d\\.ts$",
29+
"(^|/)tsconfig\\.json$",
30+
"(^|/)(babel|webpack)\\.config\\.(js|cjs|mjs|ts|json)$",
31+
"template\\.config\\.js$", // template config for Ignite CLI
32+
"bin/ignite$", // CLI entry point
33+
],
34+
},
35+
to: {},
36+
},
37+
{
38+
name: "no-deprecated-core",
39+
comment:
40+
"A module depends on a node core module that has been deprecated. Find an alternative - these are " +
41+
"bound to exist - node doesn't deprecate lightly.",
42+
severity: "warn",
43+
from: {},
44+
to: {
45+
dependencyTypes: ["core"],
46+
path: ["^(punycode)$", "^(domain)$", "^(constants)$", "^(sys)$"],
47+
},
48+
},
49+
{
50+
name: "not-to-deprecated",
51+
comment:
52+
"This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later " +
53+
"version of that module, or find an alternative. Deprecated modules are a security risk.",
54+
severity: "warn",
55+
from: {},
56+
to: {
57+
dependencyTypes: ["deprecated"],
58+
},
59+
},
60+
{
61+
name: "no-non-package-json",
62+
severity: "error",
63+
comment:
64+
"This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " +
65+
"That's problematic as the package either (1) won't be available on live (2) will be " +
66+
"available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " +
67+
"in your package.json.",
68+
from: {},
69+
to: {
70+
dependencyTypes: ["npm-no-pkg", "npm-unknown"],
71+
},
72+
},
73+
{
74+
name: "not-to-unresolvable",
75+
comment:
76+
"This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " +
77+
"module: add it to your package.json. In all other cases you likely already know what to do.",
78+
severity: "error",
79+
from: {},
80+
to: {
81+
couldNotResolve: true,
82+
},
83+
},
84+
{
85+
name: "no-duplicate-dep-types",
86+
comment:
87+
"Likely this module depends on an external ('npm') package that occurs more than once " +
88+
"in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " +
89+
"maintenance problems later on.",
90+
severity: "warn",
91+
from: {},
92+
to: {
93+
moreThanOneDependencyType: true,
94+
dependencyTypesNot: ["type-only"],
95+
},
96+
},
97+
{
98+
name: "not-to-spec",
99+
comment:
100+
"This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. " +
101+
"If there's something in a spec that's of use to other modules, it doesn't have that single " +
102+
"responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.",
103+
severity: "error",
104+
from: {
105+
pathNot: "\\.(spec|test)\\.(js|mjs|cjs|ts|tsx)$",
106+
},
107+
to: {
108+
path: "\\.(spec|test)\\.(js|mjs|cjs|ts|tsx)$",
109+
},
110+
},
111+
{
112+
name: "not-to-dev-dep",
113+
severity: "error",
114+
comment:
115+
"This module depends on an npm package from the 'devDependencies' section of your " +
116+
"package.json. It looks like something that ships to production, though. To prevent problems " +
117+
"with npm packages that aren't there on production declare it (only!) in the 'dependencies'" +
118+
"section of your package.json. If this module is development only - add it to the " +
119+
"from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration",
120+
from: {
121+
path: "^(src)",
122+
pathNot: "\\.(spec|test)\\.(js|mjs|cjs|ts|tsx)$",
123+
},
124+
to: {
125+
dependencyTypes: ["npm-dev"],
126+
pathNot: ["node_modules/@types/"],
127+
},
128+
},
129+
{
130+
name: "optional-deps-used",
131+
severity: "info",
132+
comment:
133+
"This module depends on an npm package that is declared as an optional dependency " +
134+
"in your package.json. As this makes sense in limited situations only, it's flagged here. " +
135+
"If you're using an optional dependency here by design - add an exception to your" +
136+
"dependency-cruiser configuration.",
137+
from: {},
138+
to: {
139+
dependencyTypes: ["npm-optional"],
140+
},
141+
},
142+
{
143+
name: "peer-deps-used",
144+
comment:
145+
"This module depends on an npm package that is declared as a peer dependency " +
146+
"in your package.json. This makes sense if your package is e.g. a plugin, but in " +
147+
"other cases - maybe not so much. If the use of a peer dependency is intentional " +
148+
"add an exception to your dependency-cruiser configuration.",
149+
severity: "warn",
150+
from: {},
151+
to: {
152+
dependencyTypes: ["npm-peer"],
153+
},
154+
},
155+
],
156+
options: {
157+
doNotFollow: {
158+
path: "node_modules",
159+
},
160+
tsPreCompilationDeps: true,
161+
tsConfig: {
162+
fileName: "tsconfig.json",
163+
},
164+
enhancedResolveOptions: {
165+
exportsFields: ["exports"],
166+
conditionNames: ["import", "require", "node", "default"],
167+
extensions: [".ts", ".js", ".json"],
168+
},
169+
reporterOptions: {
170+
dot: {
171+
collapsePattern: "node_modules/(@[^/]+/[^/]+|[^/]+)",
172+
},
173+
archi: {
174+
collapsePattern: "^(src|test)/[^/]+",
175+
},
176+
text: {
177+
highlightFocused: true,
178+
},
179+
},
180+
},
181+
}

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,7 @@ test/artifacts/*
4242
# Ignore build artifacts
4343
.yarn/build-state.yml
4444
.yarn/install-state.gz
45+
46+
# Dependency graph visualizations
47+
ignite-cli-dependency-graph.svg
48+
ignite-cli-dependency-graph.png

boilerplate/.dependency-cruiser.js

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
const metroConfig = require("./metro.config.js")
2+
3+
const platforms = ["ios", "android", "web", "native"]
4+
const extensions = metroConfig?.resolver?.sourceExts.flatMap((pExt) =>
5+
platforms.map((platform) => `.${platform}.${pExt}`).concat(`.${pExt}`),
6+
)
7+
8+
/** @type {import('dependency-cruiser').IConfiguration} */
9+
module.exports = {
10+
forbidden: [
11+
{
12+
name: "no-circular",
13+
severity: "warn",
14+
comment:
15+
"This dependency is part of a circular relationship. You might want to revise " +
16+
"your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ",
17+
from: {},
18+
to: {
19+
circular: true,
20+
},
21+
},
22+
{
23+
name: "no-orphans",
24+
comment:
25+
"This is an orphan module - it's likely not used (anymore?). Either use it or " +
26+
"remove it. If it's logical this module is an orphan (i.e. it's a config file), " +
27+
"add an exception for it in your dependency-cruiser configuration. By default " +
28+
"this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " +
29+
"files (.d.ts), tsconfig.json and some of the babel and webpack configs.",
30+
severity: "warn",
31+
from: {
32+
orphan: true,
33+
pathNot: [
34+
"(^|/)\\.[^/]+\\.(js|cjs|mjs|ts|json)$",
35+
"\\.d\\.ts$",
36+
"(^|/)tsconfig\\.json$",
37+
"(^|/)(babel|webpack)\\.config\\.(js|cjs|mjs|ts|json)$",
38+
"crashReporting\\.ts$", // Boilerplate file for future crash reporting setup
39+
"utils/delay\\.ts$", // Utility function for delaying execution
40+
],
41+
},
42+
to: {},
43+
},
44+
{
45+
name: "no-deprecated-core",
46+
comment:
47+
"A module depends on a node core module that has been deprecated. Find an alternative - these are " +
48+
"bound to exist - node doesn't deprecate lightly.",
49+
severity: "warn",
50+
from: {},
51+
to: {
52+
dependencyTypes: ["core"],
53+
path: ["^(punycode)$", "^(domain)$", "^(constants)$", "^(sys)$"],
54+
},
55+
},
56+
{
57+
name: "not-to-deprecated",
58+
comment:
59+
"This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later " +
60+
"version of that module, or find an alternative. Deprecated modules are a security risk.",
61+
severity: "warn",
62+
from: {},
63+
to: {
64+
dependencyTypes: ["deprecated"],
65+
},
66+
},
67+
{
68+
name: "no-non-package-json",
69+
severity: "error",
70+
comment:
71+
"This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " +
72+
"That's problematic as the package either (1) won't be available on live (2) will be " +
73+
"available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " +
74+
"in your package.json.",
75+
from: {},
76+
to: {
77+
dependencyTypes: ["npm-no-pkg", "npm-unknown"],
78+
},
79+
},
80+
{
81+
name: "not-to-unresolvable",
82+
comment:
83+
"This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " +
84+
"module: add it to your package.json. In all other cases you likely already know what to do.",
85+
severity: "error",
86+
from: {},
87+
to: {
88+
couldNotResolve: true,
89+
},
90+
},
91+
{
92+
name: "no-duplicate-dep-types",
93+
comment:
94+
"Likely this module depends on an external ('npm') package that occurs more than once " +
95+
"in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " +
96+
"maintenance problems later on.",
97+
severity: "warn",
98+
from: {},
99+
to: {
100+
moreThanOneDependencyType: true,
101+
dependencyTypesNot: ["type-only"],
102+
},
103+
},
104+
{
105+
name: "not-to-spec",
106+
comment:
107+
"This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. " +
108+
"If there's something in a spec that's of use to other modules, it doesn't have that single " +
109+
"responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.",
110+
severity: "error",
111+
from: {
112+
pathNot: "\\.(spec|test)\\.(js|mjs|cjs|ts|tsx)$",
113+
},
114+
to: {
115+
path: "\\.(spec|test)\\.(js|mjs|cjs|ts|tsx)$",
116+
},
117+
},
118+
{
119+
name: "not-to-dev-dep",
120+
severity: "error",
121+
comment:
122+
"This module depends on an npm package from the 'devDependencies' section of your " +
123+
"package.json. It looks like something that ships to production, though. To prevent problems " +
124+
"with npm packages that aren't there on production declare it (only!) in the 'dependencies'" +
125+
"section of your package.json. If this module is development only - add it to the " +
126+
"from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration",
127+
from: {
128+
path: "^(app|src)",
129+
pathNot: "\\.(spec|test)\\.(js|mjs|cjs|ts|tsx)$",
130+
},
131+
to: {
132+
dependencyTypes: ["npm-dev"],
133+
pathNot: ["node_modules/@types/"],
134+
exoticRequireNot: [
135+
"react-native/Libraries/Utilities/codegenNativeComponent",
136+
"react-native/Libraries/Utilities/codegenNativeCommands",
137+
],
138+
},
139+
},
140+
{
141+
name: "optional-deps-used",
142+
severity: "info",
143+
comment:
144+
"This module depends on an npm package that is declared as an optional dependency " +
145+
"in your package.json. As this makes sense in limited situations only, it's flagged here. " +
146+
"If you're using an optional dependency here by design - add an exception to your" +
147+
"dependency-cruiser configuration.",
148+
from: {},
149+
to: {
150+
dependencyTypes: ["npm-optional"],
151+
},
152+
},
153+
{
154+
name: "peer-deps-used",
155+
comment:
156+
"This module depends on an npm package that is declared as a peer dependency " +
157+
"in your package.json. This makes sense if your package is e.g. a plugin, but in " +
158+
"other cases - maybe not so much. If the use of a peer dependency is intentional " +
159+
"add an exception to your dependency-cruiser configuration.",
160+
severity: "warn",
161+
from: {},
162+
to: {
163+
dependencyTypes: ["npm-peer"],
164+
},
165+
},
166+
],
167+
options: {
168+
doNotFollow: {
169+
path: "node_modules",
170+
},
171+
tsPreCompilationDeps: true,
172+
tsConfig: {
173+
fileName: "tsconfig.json",
174+
},
175+
enhancedResolveOptions: {
176+
exportsFields: ["exports"],
177+
conditionNames: ["import", "require", "node", "default"],
178+
// React Native / Metro bundler support for platform-specific extensions
179+
// See: https://reactnative.dev/docs/platform-specific-code
180+
// See: https://github.com/sverweij/dependency-cruiser/issues/511
181+
extensions,
182+
},
183+
reporterOptions: {
184+
dot: {
185+
collapsePattern: "node_modules/(@[^/]+/[^/]+|[^/]+)",
186+
},
187+
archi: {
188+
collapsePattern: "^(app|src|test)/[^/]+",
189+
},
190+
text: {
191+
highlightFocused: true,
192+
},
193+
},
194+
},
195+
}

0 commit comments

Comments
 (0)