eslint-plugin-boundaries 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -228,6 +228,37 @@ Files or dependencies matching these [`micromatch` patterns](https://github.com/
228
228
 
229
229
  > Note: The `boundaries/include` option has preference over `boundaries/ignore`. If you define `boundaries/include`, use `boundaries/ignore` to ignore subsets of included files.
230
230
 
231
+ #### __`boundaries/root-path`__
232
+
233
+ Use this setting only if you are facing issues with the plugin when executing the lint command from a different path than the project root.
234
+
235
+ <details>
236
+ <summary>How to define the root path of the project</summary>
237
+
238
+ By default, the plugin uses the current working directory (`process.cwd()`) as root path of the project. This path is used as the base path when resolving file matchers from rules and `boundaries/elements` settings. This is specially important when using the `basePattern` option or the `full` mode in the `boundaries/elements` setting. This may produce unexpected results [when the lint command is executed from a different path than the project root](https://github.com/javierbrea/eslint-plugin-boundaries/issues/296). To fix this, you can define a different root path using this option.
239
+
240
+ For example, supposing that the `.eslintrc.js` file is located in the project root, you could define the root path as in:
241
+
242
+ ```js
243
+ {
244
+ settings: {
245
+ "boundaries/root-path": path.resolve(__dirname)
246
+ }
247
+ }
248
+ ```
249
+
250
+ Note that the path should be absolute and resolved before passing it to the plugin. Otherwise, it will be resolved using the current working directory, and the problem will persist. In case you are defining the configuration in a `.eslintrc.(yml|json)` file, and you don't want to hardcode an absolute path, you can use the next environment variable to define the root path when executing the lint command:
251
+
252
+ ```bash
253
+ ESLINT_PLUGIN_BOUNDARIES_ROOT_PATH=../../project-root npm run lint
254
+ ```
255
+
256
+ You can also provide an absolute path in the environment variable, but it may be more useful to use a relative path to the project root. Remember that it will be resolved from the path where the lint command is executed.
257
+
258
+ </details>
259
+
260
+
261
+
231
262
  ### Predefined configurations
232
263
 
233
264
  This plugin is distributed with two different predefined configurations: "recommended" and "strict".
@@ -261,7 +292,7 @@ Some rules require extra configuration, and it has to be defined in each specifi
261
292
 
262
293
  The docs of each rule contains an specification of their own options, but __the main rules share the format in which the options have to be defined__. The format described here is valid for options of [`element-types`](docs/rules/element-types.md), [`external`](docs/rules/external.md) and [`entry-point`](docs/rules/entry-point.md) rules.
263
294
 
264
- Options set an `allow` or `disallow` value by default, and provide an array of rules. Each matching rule will override the default value and the value returned by previous matching rules. So, the final result of the options, once processed for each case, will be `allow` or `disallow`, and this value will be applied by the plugin rule in the correspondant way, making it to produce an eslint error or not.
295
+ Options set an `allow` or `disallow` value by default, and provide an array of rules. Each matching rule will override the default value and the value returned by previous matching rules. So, the final result of the options, once processed for each case, will be `allow` or `disallow`, and this value will be applied by the plugin rule in the correspondent way, making it to produce an eslint error or not.
265
296
 
266
297
  ```jsonc
267
298
  {
@@ -277,6 +308,8 @@ Options set an `allow` or `disallow` value by default, and provide an array of r
277
308
  "from": ["helpers"],
278
309
  // ...disallow importing this type of elements
279
310
  "disallow": ["modules", "components"],
311
+ // ..for this kind of imports (applies only when using TypeScript)
312
+ "importKind": "value",
280
313
  // ...and return this custom error message
281
314
  "message": "Helpers must not import other thing than helpers"
282
315
  },
@@ -300,6 +333,7 @@ Remember that:
300
333
 
301
334
  * __`from/target`__: `<element matchers>` Depending of the rule to which the options are for, the rule will be applied only if the file being analyzed matches with this element matcher (`from`), or the dependency being imported matches with this element matcher (`target`).
302
335
  * __`disallow/allow`__: `<value matchers>` If the plugin rule target matches with this, then the result of the rule will be "disallow/allow". Each rule will require a type of value here depending of what it is checking. In the case of the `element-types` rule, for example, another `<element matcher>` has to be provided in order to check the type of the local dependency.
336
+ * __`importKind`__: `<string>` _Optional_. It is useful only when using TypeScript, as it allows to define if the rule applies when the dependency is being imported as a value or as a type. It can be also defined as an array of strings, or a micromatch pattern. Note that possible values to match with are `"value"`, `"type"` or `"typeof"`. For example, you could define that "components" can import "helpers" as a value, but not as a type. So, `import { helper } from "helpers/helper-a"` would be allowed, but `import type { Helper } from "helpers/helper-a"` would be disallowed.
303
337
  * __`message`__: `<string>` Optional. If the rule results in an error, the plugin will return this message instead of the default one. Read [error messages](#error-messages) for further info.
304
338
 
305
339
  > Tip: Properties `from/target` and `disallow/allow` can receive a single matcher, or an array of matchers.
@@ -340,6 +374,7 @@ Available properties in error templates both from `file` or `dependency` are:
340
374
  * `internalPath`: File path being analyzed or imported. Relative to the element's root path.
341
375
  * `source`: Available only for `dependency`. The source of the `import` statement as it is in the code.
342
376
  * `parent`: If the element is child of another element, it is also available in this property, which contains correspondent `type`, `internalPath` and captured properties as well.
377
+ * `importKind`: Available only for `dependency` when using TypeScript. It contains the kind of import being analyzed. Possible values are `"value"`, `"type"` or `"typeof"`.
343
378
  * ...All captured properties are also available
344
379
 
345
380
  > Tip: Read ["Global settings"](#global-settings) for further info about how to capture values from elements.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-boundaries",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "Eslint plugin checking architecture boundaries between elements",
5
5
  "keywords": [
6
6
  "eslint",
@@ -32,21 +32,24 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "chalk": "4.1.2",
35
- "eslint-import-resolver-node": "0.3.7",
36
- "eslint-module-utils": "2.7.4",
37
- "is-core-module": "2.11.0",
35
+ "eslint-import-resolver-node": "0.3.9",
36
+ "eslint-module-utils": "2.8.0",
37
+ "is-core-module": "2.13.0",
38
38
  "micromatch": "4.0.5"
39
39
  },
40
40
  "devDependencies": {
41
+ "@typescript-eslint/eslint-plugin": "6.4.0",
42
+ "@typescript-eslint/parser": "6.4.0",
41
43
  "cross-env": "7.0.3",
42
- "eslint": "8.37.0",
43
- "eslint-config-prettier": "8.8.0",
44
- "eslint-plugin-prettier": "4.2.1",
44
+ "eslint": "8.47.0",
45
+ "eslint-config-prettier": "9.0.0",
46
+ "eslint-import-resolver-typescript": "3.6.0",
47
+ "eslint-plugin-prettier": "5.0.0",
45
48
  "husky": "8.0.3",
46
49
  "is-ci": "3.0.1",
47
- "jest": "29.5.0",
48
- "lint-staged": "13.2.0",
49
- "prettier": "2.8.7"
50
+ "jest": "29.6.2",
51
+ "lint-staged": "14.0.0",
52
+ "prettier": "3.0.1"
50
53
  },
51
54
  "lint-staged": {
52
55
  "test/**/*.js": "eslint",
@@ -1,5 +1,7 @@
1
1
  const PLUGIN_NAME = "boundaries";
2
+ const PLUGIN_ENV_VARS_PREFIX = "ESLINT_PLUGIN_BOUNDARIES";
2
3
 
3
4
  module.exports = {
4
5
  PLUGIN_NAME,
6
+ PLUGIN_ENV_VARS_PREFIX,
5
7
  };
@@ -1,4 +1,4 @@
1
- const { PLUGIN_NAME } = require("./plugin");
1
+ const { PLUGIN_NAME, PLUGIN_ENV_VARS_PREFIX } = require("./plugin");
2
2
 
3
3
  const {
4
4
  ELEMENT_TYPES,
@@ -15,6 +15,11 @@ module.exports = {
15
15
  ELEMENTS: `${PLUGIN_NAME}/elements`,
16
16
  IGNORE: `${PLUGIN_NAME}/ignore`,
17
17
  INCLUDE: `${PLUGIN_NAME}/include`,
18
+ ROOT_PATH: `${PLUGIN_NAME}/root-path`,
19
+
20
+ // env vars
21
+ DEBUG: `${PLUGIN_ENV_VARS_PREFIX}_DEBUG`,
22
+ ENV_ROOT_PATH: `${PLUGIN_ENV_VARS_PREFIX}_ROOT_PATH`,
18
23
 
19
24
  // rules
20
25
  RULE_ELEMENT_TYPES: `${PLUGIN_NAME}/${ELEMENT_TYPES}`,
@@ -1,8 +1,7 @@
1
1
  const { fileInfo, importInfo } = require("./elementsInfo");
2
2
 
3
3
  function getParent(elementInfo) {
4
- const parent = elementInfo.parents && elementInfo.parents[0];
5
- return parent && parent.elementPath;
4
+ return elementInfo.parents?.[0]?.elementPath;
6
5
  }
7
6
 
8
7
  function getCommonAncestor(elementInfoA, elementInfoB) {
@@ -11,7 +10,7 @@ function getCommonAncestor(elementInfoA, elementInfoB) {
11
10
  return elementParentA.elementPath === elementParentB.elementPath;
12
11
  });
13
12
  });
14
- return commonAncestor && commonAncestor.elementPath;
13
+ return commonAncestor?.elementPath;
15
14
  }
16
15
 
17
16
  function isUncle(elementA, elementB) {
@@ -65,12 +64,13 @@ function dependencyRelationship(dependency, element) {
65
64
  return null;
66
65
  }
67
66
 
68
- function dependencyInfo(source, context) {
67
+ function dependencyInfo(source, importKind, context) {
69
68
  const elementInfo = fileInfo(context);
70
69
  const dependency = importInfo(source, context);
71
70
 
72
71
  return {
73
72
  ...dependency,
73
+ importKind: importKind || "value",
74
74
  relationship: dependencyRelationship(dependency, elementInfo),
75
75
  isInternal: isInternal(dependency, elementInfo),
76
76
  };
@@ -3,7 +3,7 @@ const micromatch = require("micromatch");
3
3
  const resolve = require("eslint-module-utils/resolve").default;
4
4
 
5
5
  const { IGNORE, INCLUDE, VALID_MODES } = require("../constants/settings");
6
- const { getElements } = require("../helpers/settings");
6
+ const { getElements, getRootPath } = require("../helpers/settings");
7
7
  const { debugFileInfo } = require("../helpers/debug");
8
8
  const { isArray } = require("../helpers/utils");
9
9
 
@@ -126,12 +126,12 @@ function elementTypeAndParents(path, settings) {
126
126
  path
127
127
  .split("/")
128
128
  .slice(0, path.split("/").length - lastSegmentMatching)
129
- .join("/")
129
+ .join("/"),
130
130
  );
131
131
  }
132
132
  const capture = micromatch.capture(
133
133
  pattern,
134
- useFullPathMatch ? path : accumulator.join("/")
134
+ useFullPathMatch ? path : accumulator.join("/"),
135
135
  );
136
136
 
137
137
  if (capture && basePatternCapture) {
@@ -171,7 +171,7 @@ function elementTypeAndParents(path, settings) {
171
171
  });
172
172
  return { accumulator, lastSegmentMatching };
173
173
  },
174
- { accumulator: [], lastSegmentMatching: 0 }
174
+ { accumulator: [], lastSegmentMatching: 0 },
175
175
  );
176
176
 
177
177
  return {
@@ -184,9 +184,9 @@ function replacePathSlashes(absolutePath) {
184
184
  return absolutePath.replace(/\\/g, "/");
185
185
  }
186
186
 
187
- function projectPath(absolutePath) {
187
+ function projectPath(absolutePath, rootPath) {
188
188
  if (absolutePath) {
189
- return replacePathSlashes(absolutePath).replace(`${replacePathSlashes(process.cwd())}/`, "");
189
+ return replacePathSlashes(absolutePath).replace(`${replacePathSlashes(rootPath)}/`, "");
190
190
  }
191
191
  }
192
192
 
@@ -195,7 +195,7 @@ function externalModulePath(source, baseModuleValue) {
195
195
  }
196
196
 
197
197
  function importInfo(source, context) {
198
- const path = projectPath(resolve(source, context));
198
+ const path = projectPath(resolve(source, context), getRootPath(context.settings));
199
199
  const isExternalModule = isExternal(source, path);
200
200
  const resultCache = importsCache.load(isExternalModule ? source : path, context.settings);
201
201
  let elementCache;
@@ -237,7 +237,7 @@ function importInfo(source, context) {
237
237
  }
238
238
 
239
239
  function fileInfo(context) {
240
- const path = projectPath(context.getFilename());
240
+ const path = projectPath(context.getFilename(), getRootPath(context.settings));
241
241
  const resultCache = filesCache.load(path, context.settings);
242
242
  let elementCache;
243
243
  let result;
@@ -1,6 +1,7 @@
1
1
  const chalk = require("chalk");
2
2
 
3
3
  const { PLUGIN_NAME } = require("../constants/plugin");
4
+ const { isDebugModeEnabled } = require("./settings");
4
5
 
5
6
  const warns = [];
6
7
  const debuggedFiles = [];
@@ -13,12 +14,6 @@ function warn(message) {
13
14
  trace(message, "yellow");
14
15
  }
15
16
 
16
- function debug(message) {
17
- if (process.env.ESLINT_PLUGIN_BOUNDARIES_DEBUG) {
18
- trace(message, "grey");
19
- }
20
- }
21
-
22
17
  function success(message) {
23
18
  trace(message, "green");
24
19
  }
@@ -32,7 +27,7 @@ function warnOnce(message) {
32
27
 
33
28
  function debugFileInfo(fileInfo) {
34
29
  const fileInfoKey = fileInfo.path || fileInfo.source;
35
- if (process.env.ESLINT_PLUGIN_BOUNDARIES_DEBUG && !debuggedFiles.includes(fileInfoKey)) {
30
+ if (isDebugModeEnabled() && !debuggedFiles.includes(fileInfoKey)) {
36
31
  debuggedFiles.push(fileInfoKey);
37
32
  if (fileInfo.type) {
38
33
  success(`'${fileInfoKey}' is of type '${fileInfo.type}'`);
@@ -44,7 +39,6 @@ function debugFileInfo(fileInfo) {
44
39
  }
45
40
 
46
41
  module.exports = {
47
- debug,
48
42
  success,
49
43
  debugFileInfo,
50
44
  warnOnce,
@@ -22,7 +22,7 @@ function propertiesConcater(properties, index) {
22
22
  function micromatchPatternMessage(micromatchPatterns, elementCapturedValues) {
23
23
  const micromatchPatternsWithValues = micromatchPatternReplacingObjectsValues(
24
24
  micromatchPatterns,
25
- { from: elementCapturedValues }
25
+ { from: elementCapturedValues },
26
26
  );
27
27
  if (isArray(micromatchPatternsWithValues)) {
28
28
  if (micromatchPatternsWithValues.length === 1) {
@@ -60,7 +60,7 @@ function elementMatcherMessage(elementMatcher, elementCapturedValues) {
60
60
  }
61
61
  return `${typeMessage(elementMatcher[0])}${capturedValuesMatcherMessage(
62
62
  elementMatcher[1],
63
- elementCapturedValues
63
+ elementCapturedValues,
64
64
  )}`;
65
65
  }
66
66
 
@@ -85,6 +85,7 @@ function elementPropertiesToReplaceInTemplate(element) {
85
85
  type: element.type,
86
86
  internalPath: element.internalPath,
87
87
  source: element.source,
88
+ importKind: element.importKind,
88
89
  };
89
90
  }
90
91
 
@@ -92,39 +93,39 @@ function customErrorMessage(message, file, dependency, report = {}) {
92
93
  let replacedMessage = replaceObjectValuesInTemplates(
93
94
  replaceObjectValuesInTemplates(message, elementPropertiesToReplaceInTemplate(file), "file"),
94
95
  elementPropertiesToReplaceInTemplate(dependency),
95
- "dependency"
96
+ "dependency",
96
97
  );
97
98
  replacedMessage = replaceObjectValuesInTemplates(
98
99
  replaceObjectValuesInTemplates(
99
100
  replacedMessage,
100
101
  elementPropertiesToReplaceInTemplate(file),
101
- "from"
102
+ "from",
102
103
  ),
103
104
  elementPropertiesToReplaceInTemplate(dependency),
104
- "target"
105
+ "target",
105
106
  );
106
107
  if (file.parents[0]) {
107
108
  replacedMessage = replaceObjectValuesInTemplates(
108
109
  replacedMessage,
109
110
  elementPropertiesToReplaceInTemplate(file.parents[0]),
110
- "file.parent"
111
+ "file.parent",
111
112
  );
112
113
  replacedMessage = replaceObjectValuesInTemplates(
113
114
  replacedMessage,
114
115
  elementPropertiesToReplaceInTemplate(file.parents[0]),
115
- "from.parent"
116
+ "from.parent",
116
117
  );
117
118
  }
118
119
  if (dependency.parents[0]) {
119
120
  replacedMessage = replaceObjectValuesInTemplates(
120
121
  replacedMessage,
121
122
  elementPropertiesToReplaceInTemplate(dependency.parents[0]),
122
- "dependency.parent"
123
+ "dependency.parent",
123
124
  );
124
125
  replacedMessage = replaceObjectValuesInTemplates(
125
126
  replacedMessage,
126
127
  elementPropertiesToReplaceInTemplate(dependency.parents[0]),
127
- "target.parent"
128
+ "target.parent",
128
129
  );
129
130
  }
130
131
  return replaceObjectValuesInTemplates(replacedMessage, report, "report");
@@ -148,13 +149,37 @@ function elementCapturedValuesMessage(capturedValues) {
148
149
 
149
150
  function elementMessage(elementInfo) {
150
151
  return `of type ${quote(elementInfo.type)}${elementCapturedValuesMessage(
151
- elementInfo.capturedValues
152
+ elementInfo.capturedValues,
152
153
  )}`;
153
154
  }
154
155
 
156
+ function hasToPrintKindMessage(ruleImportKind, dependencyInfo) {
157
+ return ruleImportKind && dependencyInfo.importKind;
158
+ }
159
+
160
+ function dependencyImportKindMessage(ruleImportKind, dependencyInfo) {
161
+ if (hasToPrintKindMessage(ruleImportKind, dependencyInfo)) {
162
+ return `kind ${quote(dependencyInfo.importKind)} from `;
163
+ }
164
+ return "";
165
+ }
166
+
167
+ function dependencyUsageKindMessage(
168
+ ruleImportKind,
169
+ dependencyInfo,
170
+ { suffix = " ", prefix = "" } = {},
171
+ ) {
172
+ if (hasToPrintKindMessage(ruleImportKind, dependencyInfo)) {
173
+ return `${prefix}${dependencyInfo.importKind}${suffix}`;
174
+ }
175
+ return "";
176
+ }
177
+
155
178
  module.exports = {
156
179
  quote,
157
180
  ruleElementMessage,
158
181
  customErrorMessage,
159
182
  elementMessage,
183
+ dependencyImportKindMessage,
184
+ dependencyUsageKindMessage,
160
185
  };
@@ -62,9 +62,12 @@ function micromatchPatternReplacingObjectsValues(pattern, object) {
62
62
  function isObjectMatch(objectWithMatchers, object, objectsWithValuesToReplace) {
63
63
  return Object.keys(objectWithMatchers).reduce((isMatch, key) => {
64
64
  if (isMatch) {
65
+ if (!object) {
66
+ return false;
67
+ }
65
68
  const micromatchPattern = micromatchPatternReplacingObjectsValues(
66
69
  objectWithMatchers[key],
67
- objectsWithValuesToReplace
70
+ objectsWithValuesToReplace,
68
71
  );
69
72
  return micromatch.isMatch(object[key], micromatchPattern);
70
73
  }
@@ -76,17 +79,23 @@ function rulesMainKey(key) {
76
79
  return key || FROM;
77
80
  }
78
81
 
79
- function ruleMatch(ruleMatchers, targetElement, isMatch, fromElement) {
82
+ function ruleMatch(ruleMatchers, targetElement, isMatch, fromElement, importKind) {
80
83
  let match = { result: false, report: null };
81
84
  const matchers = !isArray(ruleMatchers) ? [ruleMatchers] : ruleMatchers;
82
85
  matchers.forEach((matcher) => {
83
86
  if (!match.result) {
84
87
  if (isArray(matcher)) {
85
88
  const [value, captures] = matcher;
86
- match = isMatch(targetElement, value, captures, {
87
- from: fromElement.capturedValues,
88
- target: targetElement.capturedValues,
89
- });
89
+ match = isMatch(
90
+ targetElement,
91
+ value,
92
+ captures,
93
+ {
94
+ from: fromElement.capturedValues,
95
+ target: targetElement.capturedValues,
96
+ },
97
+ importKind,
98
+ );
90
99
  } else {
91
100
  match = isMatch(
92
101
  targetElement,
@@ -95,7 +104,8 @@ function ruleMatch(ruleMatchers, targetElement, isMatch, fromElement) {
95
104
  {
96
105
  from: fromElement.capturedValues,
97
106
  target: targetElement.capturedValues,
98
- }
107
+ },
108
+ importKind,
99
109
  );
100
110
  }
101
111
  }
@@ -108,11 +118,11 @@ function isMatchElementKey(
108
118
  matcher,
109
119
  options,
110
120
  elementKey,
111
- elementsToCompareCapturedValues
121
+ elementsToCompareCapturedValues,
112
122
  ) {
113
123
  const isMatch = micromatch.isMatch(
114
124
  elementInfo[elementKey],
115
- micromatchPatternReplacingObjectsValues(matcher, elementsToCompareCapturedValues)
125
+ micromatchPatternReplacingObjectsValues(matcher, elementsToCompareCapturedValues),
116
126
  );
117
127
  if (isMatch && options) {
118
128
  return {
@@ -124,7 +134,23 @@ function isMatchElementKey(
124
134
  };
125
135
  }
126
136
 
127
- function isMatchElementType(elementInfo, matcher, options, elementsToCompareCapturedValues) {
137
+ function isMatchImportKind(elementInfo, importKind) {
138
+ if (!elementInfo.importKind || !importKind) {
139
+ return true;
140
+ }
141
+ return micromatch.isMatch(elementInfo.importKind, importKind);
142
+ }
143
+
144
+ function isMatchElementType(
145
+ elementInfo,
146
+ matcher,
147
+ options,
148
+ elementsToCompareCapturedValues,
149
+ importKind,
150
+ ) {
151
+ if (!isMatchImportKind(elementInfo, importKind)) {
152
+ return { result: false };
153
+ }
128
154
  return isMatchElementKey(elementInfo, matcher, options, "type", elementsToCompareCapturedValues);
129
155
  }
130
156
 
@@ -166,11 +192,11 @@ function elementRulesAllowDependency({
166
192
  const [result, report, ruleReport] = getElementRules(
167
193
  elementToGetRulesFrom(element, dependency, mainKey),
168
194
  options,
169
- mainKey
195
+ mainKey,
170
196
  ).reduce(
171
197
  (allowed, rule) => {
172
198
  if (rule.disallow) {
173
- const match = ruleMatch(rule.disallow, dependency, isMatch, element);
199
+ const match = ruleMatch(rule.disallow, dependency, isMatch, element, rule.importKind);
174
200
  if (match.result) {
175
201
  return [
176
202
  false,
@@ -180,12 +206,13 @@ function elementRulesAllowDependency({
180
206
  disallow: rule.disallow,
181
207
  index: rule.index,
182
208
  message: rule.message || options.message,
209
+ importKind: rule.importKind,
183
210
  },
184
211
  ];
185
212
  }
186
213
  }
187
214
  if (rule.allow) {
188
- const match = ruleMatch(rule.allow, dependency, isMatch, element);
215
+ const match = ruleMatch(rule.allow, dependency, isMatch, element, rule.importKind);
189
216
  if (match.result) {
190
217
  return [true, match.report];
191
218
  }
@@ -199,7 +226,7 @@ function elementRulesAllowDependency({
199
226
  isDefault: true,
200
227
  message: options.message,
201
228
  },
202
- ]
229
+ ],
203
230
  );
204
231
  return {
205
232
  result,
@@ -218,4 +245,5 @@ module.exports = {
218
245
  getElementRules,
219
246
  rulesMainKey,
220
247
  micromatchPatternReplacingObjectsValues,
248
+ isMatchImportKind,
221
249
  };
@@ -1,5 +1,13 @@
1
- const { TYPES, ELEMENTS, VALID_MODES } = require("../constants/settings");
1
+ const {
2
+ TYPES,
3
+ ELEMENTS,
4
+ VALID_MODES,
5
+ ROOT_PATH,
6
+ ENV_ROOT_PATH,
7
+ DEBUG,
8
+ } = require("../constants/settings");
2
9
  const { isString } = require("./utils");
10
+ const { isAbsolute, resolve } = require("path");
3
11
 
4
12
  function isLegacyType(type) {
5
13
  return isString(type);
@@ -34,8 +42,24 @@ function getElementsTypeNames(settings) {
34
42
  return getElements(settings).map((element) => element.type);
35
43
  }
36
44
 
45
+ function getRootPath(settings) {
46
+ const rootPathUserSetting = process.env[ENV_ROOT_PATH] || settings[ROOT_PATH];
47
+ if (rootPathUserSetting) {
48
+ return isAbsolute(rootPathUserSetting)
49
+ ? rootPathUserSetting
50
+ : resolve(process.cwd(), rootPathUserSetting);
51
+ }
52
+ return process.cwd();
53
+ }
54
+
55
+ function isDebugModeEnabled() {
56
+ return process.env[DEBUG];
57
+ }
58
+
37
59
  module.exports = {
38
60
  isLegacyType,
39
61
  getElements,
40
62
  getElementsTypeNames,
63
+ isDebugModeEnabled,
64
+ getRootPath,
41
65
  };
@@ -8,7 +8,8 @@ function isArray(object) {
8
8
 
9
9
  function replaceObjectValueInTemplate(template, key, value, namespace) {
10
10
  const keyToReplace = namespace ? `${namespace}.${key}` : key;
11
- return template.replace(`\${${keyToReplace}}`, value);
11
+ const regexp = new RegExp(`\\$\\{${keyToReplace}\\}`, "g");
12
+ return template.replace(regexp, value);
12
13
  }
13
14
 
14
15
  function replaceObjectValuesInTemplates(strings, object, namespace) {
@@ -63,6 +63,19 @@ function rulesOptionsSchema(options = {}) {
63
63
  [mainKey]: elementsMatcherSchema(),
64
64
  allow: elementsMatcherSchema(options.targetMatcherOptions),
65
65
  disallow: elementsMatcherSchema(options.targetMatcherOptions),
66
+ importKind: {
67
+ oneOf: [
68
+ {
69
+ type: "string",
70
+ },
71
+ {
72
+ type: "array",
73
+ items: {
74
+ type: "string",
75
+ },
76
+ },
77
+ ],
78
+ },
66
79
  message: {
67
80
  type: "string",
68
81
  },
@@ -109,7 +122,7 @@ function validateElements(elements) {
109
122
  // TODO, remove in next major version
110
123
  if (isLegacyType(element)) {
111
124
  warnOnce(
112
- `Defining elements as strings in settings is deprecated. Will be automatically converted, but this feature will be removed in next major versions`
125
+ `Defining elements as strings in settings is deprecated. Will be automatically converted, but this feature will be removed in next major versions`,
113
126
  );
114
127
  } else {
115
128
  Object.keys(element).forEach(() => {
@@ -119,8 +132,8 @@ function validateElements(elements) {
119
132
  if (element.mode && !VALID_MODES.includes(element.mode)) {
120
133
  warnOnce(
121
134
  `Invalid mode property in '${ELEMENTS}' setting. Should be one of ${VALID_MODES.join(
122
- ","
123
- )}. Default value "${VALID_MODES[0]}" will be used instead`
135
+ ",",
136
+ )}. Default value "${VALID_MODES[0]}" will be used instead`,
124
137
  );
125
138
  }
126
139
  if (!element.pattern || !(isString(element.pattern) || isArray(element.pattern))) {
@@ -137,7 +150,7 @@ function validateElements(elements) {
137
150
  function deprecateAlias(aliases) {
138
151
  if (aliases) {
139
152
  warnOnce(
140
- `Defining aliases in '${ALIAS}' setting is deprecated. Please use 'import/resolver' setting`
153
+ `Defining aliases in '${ALIAS}' setting is deprecated. Please use 'import/resolver' setting`,
141
154
  );
142
155
  }
143
156
  }
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ const packageJson = require("../package.json");
1
2
  const rules = require("./constants/rules");
2
3
  const recommendedConfig = require("./configs/recommended");
3
4
  const strictConfig = require("./configs/strict");
@@ -17,6 +18,10 @@ const importRules = (ruleNames) => {
17
18
  // export all configs
18
19
 
19
20
  module.exports = {
21
+ meta: {
22
+ name: packageJson.name,
23
+ version: packageJson.version,
24
+ },
20
25
  rules: importRules(rules),
21
26
  configs: {
22
27
  recommended: recommendedConfig,
@@ -8,7 +8,12 @@ const {
8
8
  isMatchElementType,
9
9
  elementRulesAllowDependency,
10
10
  } = require("../helpers/rules");
11
- const { customErrorMessage, ruleElementMessage, elementMessage } = require("../helpers/messages");
11
+ const {
12
+ customErrorMessage,
13
+ ruleElementMessage,
14
+ elementMessage,
15
+ dependencyImportKindMessage,
16
+ } = require("../helpers/messages");
12
17
 
13
18
  function elementRulesAllowDependencyType(element, dependency, options) {
14
19
  return elementRulesAllowDependency({
@@ -26,15 +31,18 @@ function errorMessage(ruleData, file, dependency) {
26
31
  }
27
32
  if (ruleReport.isDefault) {
28
33
  return `No rule allowing this dependency was found. File is ${elementMessage(
29
- file
34
+ file,
30
35
  )}. Dependency is ${elementMessage(dependency)}`;
31
36
  }
32
- return `Importing ${ruleElementMessage(
37
+ return `Importing ${dependencyImportKindMessage(
38
+ ruleReport.importKind,
39
+ dependency,
40
+ )}${ruleElementMessage(
33
41
  ruleReport.disallow,
34
- file.capturedValues
42
+ file.capturedValues,
35
43
  )} is not allowed in ${ruleElementMessage(
36
44
  ruleReport.element,
37
- file.capturedValues
45
+ file.capturedValues,
38
46
  )}. Disallowed in rule ${ruleReport.index + 1}`;
39
47
  }
40
48
 
@@ -55,5 +63,5 @@ module.exports = dependencyRule(
55
63
  });
56
64
  }
57
65
  }
58
- }
66
+ },
59
67
  );
@@ -7,10 +7,25 @@ const {
7
7
  dependencyLocation,
8
8
  isMatchElementKey,
9
9
  elementRulesAllowDependency,
10
+ isMatchImportKind,
10
11
  } = require("../helpers/rules");
11
- const { customErrorMessage, ruleElementMessage, elementMessage } = require("../helpers/messages");
12
+ const {
13
+ customErrorMessage,
14
+ ruleElementMessage,
15
+ elementMessage,
16
+ dependencyUsageKindMessage,
17
+ } = require("../helpers/messages");
12
18
 
13
- function isMatchElementInternalPath(elementInfo, matcher, options, elementsCapturedValues) {
19
+ function isMatchElementInternalPath(
20
+ elementInfo,
21
+ matcher,
22
+ options,
23
+ elementsCapturedValues,
24
+ importKind,
25
+ ) {
26
+ if (!isMatchImportKind(elementInfo, importKind)) {
27
+ return { result: false };
28
+ }
14
29
  return isMatchElementKey(elementInfo, matcher, options, "internalPath", elementsCapturedValues);
15
30
  }
16
31
 
@@ -36,8 +51,11 @@ function errorMessage(ruleData, file, dependency) {
36
51
  }
37
52
  return `The entry point '${dependency.internalPath}' is not allowed in ${ruleElementMessage(
38
53
  ruleReport.element,
39
- dependency.capturedValues
40
- )}. Disallowed in rule ${ruleReport.index + 1}`;
54
+ dependency.capturedValues,
55
+ )}${dependencyUsageKindMessage(ruleReport.importKind, dependency, {
56
+ prefix: " when importing ",
57
+ suffix: "",
58
+ })}. Disallowed in rule ${ruleReport.index + 1}`;
41
59
  }
42
60
 
43
61
  module.exports = dependencyRule(
@@ -62,5 +80,5 @@ module.exports = dependencyRule(
62
80
  },
63
81
  {
64
82
  validateRules: { onlyMainKey: true, mainKey: "target" },
65
- }
83
+ },
66
84
  );
@@ -9,8 +9,14 @@ const {
9
9
  dependencyLocation,
10
10
  elementRulesAllowDependency,
11
11
  micromatchPatternReplacingObjectsValues,
12
+ isMatchImportKind,
12
13
  } = require("../helpers/rules");
13
- const { customErrorMessage, ruleElementMessage, elementMessage } = require("../helpers/messages");
14
+ const {
15
+ customErrorMessage,
16
+ ruleElementMessage,
17
+ elementMessage,
18
+ dependencyUsageKindMessage,
19
+ } = require("../helpers/messages");
14
20
  const { isArray } = require("../helpers/utils");
15
21
 
16
22
  function specifiersMatch(specifiers, specifierOptions, elementsCapturedValues) {
@@ -22,7 +28,7 @@ function specifiersMatch(specifiers, specifierOptions, elementsCapturedValues) {
22
28
  return specifierOptions.reduce((found, option) => {
23
29
  const matcherWithTemplateReplaced = micromatchPatternReplacingObjectsValues(
24
30
  option,
25
- elementsCapturedValues
31
+ elementsCapturedValues,
26
32
  );
27
33
  if (micromatch.some(importedSpecifiersNames, matcherWithTemplateReplaced)) {
28
34
  found.push(option);
@@ -39,7 +45,7 @@ function pathMatch(path, pathOptions, elementsCapturedValues) {
39
45
  }
40
46
  const matcherWithTemplateReplaced = micromatchPatternReplacingObjectsValues(
41
47
  option,
42
- elementsCapturedValues
48
+ elementsCapturedValues,
43
49
  );
44
50
  if (micromatch.some(path, matcherWithTemplateReplaced)) {
45
51
  isMatch = true;
@@ -48,11 +54,20 @@ function pathMatch(path, pathOptions, elementsCapturedValues) {
48
54
  }, false);
49
55
  }
50
56
 
51
- function isMatchExternalDependency(dependency, matcher, options, elementsCapturedValues) {
57
+ function isMatchExternalDependency(
58
+ dependency,
59
+ matcher,
60
+ options,
61
+ elementsCapturedValues,
62
+ importKind,
63
+ ) {
52
64
  const matcherWithTemplatesReplaced = micromatchPatternReplacingObjectsValues(
53
65
  matcher,
54
- elementsCapturedValues
66
+ elementsCapturedValues,
55
67
  );
68
+ if (!isMatchImportKind(dependency, importKind)) {
69
+ return { result: false };
70
+ }
56
71
  const isMatch = micromatch.isMatch(dependency.baseModule, matcherWithTemplatesReplaced);
57
72
  if (isMatch && options && Object.keys(options).length) {
58
73
  const isPathMatch = options.path
@@ -62,7 +77,7 @@ function isMatchExternalDependency(dependency, matcher, options, elementsCapture
62
77
  const specifiersResult = specifiersMatch(
63
78
  dependency.specifiers,
64
79
  options.specifiers,
65
- elementsCapturedValues
80
+ elementsCapturedValues,
66
81
  );
67
82
  return {
68
83
  result: specifiersResult.length > 0,
@@ -103,9 +118,8 @@ function errorMessage(ruleData, file, dependency) {
103
118
  const ruleReport = ruleData.ruleReport;
104
119
  if (ruleReport.message) {
105
120
  return customErrorMessage(ruleReport.message, file, dependency, {
106
- specifiers:
107
- ruleData.report && ruleData.report.specifiers && ruleData.report.specifiers.join(", "),
108
- path: ruleData.report && ruleData.report.path,
121
+ specifiers: ruleData.report?.specifiers?.join(", "),
122
+ path: ruleData.report?.path,
109
123
  });
110
124
  }
111
125
  if (ruleReport.isDefault) {
@@ -116,15 +130,20 @@ function errorMessage(ruleData, file, dependency) {
116
130
 
117
131
  const fileReport = `is not allowed in ${ruleElementMessage(
118
132
  ruleReport.element,
119
- file.capturedValues
133
+ file.capturedValues,
120
134
  )}. Disallowed in rule ${ruleReport.index + 1}`;
121
135
 
122
136
  if (ruleData.report) {
123
- return `Usage of '${getErrorReportMessage(ruleData.report)}' from external module '${
137
+ return `Usage of ${dependencyUsageKindMessage(
138
+ ruleReport.importKind,
139
+ dependency,
140
+ )}'${getErrorReportMessage(ruleData.report)}' from external module '${
124
141
  dependency.baseModule
125
142
  }' ${fileReport}`;
126
143
  }
127
- return `Usage of external module '${dependency.baseModule}' ${fileReport}`;
144
+ return `Usage of ${dependencyUsageKindMessage(ruleReport.importKind, dependency, {
145
+ suffix: " from ",
146
+ })}external module '${dependency.baseModule}' ${fileReport}`;
128
147
  }
129
148
 
130
149
  module.exports = dependencyRule(
@@ -164,7 +183,7 @@ module.exports = dependencyRule(
164
183
  const ruleData = elementRulesAllowExternalDependency(
165
184
  file,
166
185
  { ...dependency, specifiers: node.source.parent.specifiers },
167
- options
186
+ options,
168
187
  );
169
188
  if (!ruleData.result) {
170
189
  context.report({
@@ -177,5 +196,5 @@ module.exports = dependencyRule(
177
196
  },
178
197
  {
179
198
  validateRules: { onlyMainKey: true },
180
- }
199
+ },
181
200
  );
@@ -20,5 +20,5 @@ module.exports = dependencyRule(
20
20
  },
21
21
  {
22
22
  validate: false,
23
- }
23
+ },
24
24
  );
@@ -40,7 +40,7 @@ module.exports = dependencyRule(
40
40
  dependency.relationship !== "internal" &&
41
41
  dependency.relationship !== "child" &&
42
42
  dependency.relationship !== "brother" &&
43
- (!options || !options.allowUncles || dependency.relationship !== "uncle")
43
+ (!options?.allowUncles || dependency.relationship !== "uncle")
44
44
  ) {
45
45
  context.report({
46
46
  message: errorMessage(file, dependency, options),
@@ -51,5 +51,5 @@ module.exports = dependencyRule(
51
51
  },
52
52
  {
53
53
  validate: false,
54
- }
54
+ },
55
55
  );
@@ -20,5 +20,5 @@ module.exports = dependencyRule(
20
20
  },
21
21
  {
22
22
  validate: false,
23
- }
23
+ },
24
24
  );
@@ -21,7 +21,7 @@ module.exports = function (ruleMeta, rule, ruleOptions = {}) {
21
21
 
22
22
  return {
23
23
  ImportDeclaration: (node) => {
24
- const dependency = dependencyInfo(node.source.value, context);
24
+ const dependency = dependencyInfo(node.source.value, node.importKind, context);
25
25
 
26
26
  rule({ file, dependency, options, node, context });
27
27
  },