eslint-plugin-boundaries 2.4.2 → 2.7.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
@@ -1,6 +1,6 @@
1
1
  [![Build status][build-image]][build-url] [![Coverage Status][coveralls-image]][coveralls-url] [![Quality Gate][quality-gate-image]][quality-gate-url]
2
2
 
3
- [![NPM dependencies][npm-dependencies-image]][npm-dependencies-url] [![Renovate](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com) [![Last commit][last-commit-image]][last-commit-url] [![Last release][release-image]][release-url]
3
+ [![Renovate](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com) [![Last commit][last-commit-image]][last-commit-url] [![Last release][release-image]][release-url]
4
4
 
5
5
  [![NPM downloads][npm-downloads-image]][npm-downloads-url] [![License][license-image]][license-url]
6
6
 
@@ -29,6 +29,7 @@ __This plugin ensures that your architecture boundaries are respected by the ele
29
29
  * [Rules configuration](#rules-configuration)
30
30
  * [Main format of rules options](#main-format-of-rules-options)
31
31
  * [Elements matchers](#elements-matchers)
32
+ * [Error messages](#error-messages)
32
33
  * [Advanced example](#advanced-example)
33
34
  - [Resolvers](#resolvers)
34
35
  - [Usage with TypeScript](#usage-with-typescript)
@@ -199,7 +200,7 @@ Define patterns to recognize each file in the project as one of this element typ
199
200
  }
200
201
  ```
201
202
 
202
- > Tip: You can enable the [debug mode](debug-mode) when configuring the plugin, and you will get information about the type assigned to each file in the project.
203
+ > Tip: You can enable the [debug mode](#debug-mode) when configuring the plugin, and you will get information about the type assigned to each file in the project, as well as captured properties and values.
203
204
 
204
205
  #### __`boundaries/include`__
205
206
 
@@ -258,23 +259,31 @@ Some rules require extra configuration, and it has to be defined in each specifi
258
259
 
259
260
  #### Main format of rules options
260
261
 
261
- 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.
262
+ 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, __except the `message` property, which for the moment is only supported in the [`element-types`](docs/rules/element-types.md) and [`entry-point`](docs/rules/entry-point.md) rules settings.
262
263
 
263
264
  Options set an "allow/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.
264
265
 
265
- ```json
266
+ ```jsonc
266
267
  {
267
268
  "rules": {
268
269
  "boundaries/element-types": [2, {
270
+ // Allow or disallow any dependency by default
269
271
  "default": "allow",
272
+ // Define a custom message for this rule
273
+ "message": "${file.type} is not allowed to import ${dependency.type}",
270
274
  "rules": [
271
275
  {
276
+ // In this type of files...
272
277
  "from": ["helpers"],
273
- "disallow": ["modules", "components", "helpers"]
278
+ // ...disallow importing this type of elements
279
+ "disallow": ["modules", "components"],
280
+ // ...and return this custom error message
281
+ "message": "Helpers must not import other thing than helpers"
274
282
  },
275
283
  {
276
284
  "from": ["components"],
277
285
  "disallow": ["modules"]
286
+ // As this rule has not "message" property, it will use the message defined at first level
278
287
  }
279
288
  ]
280
289
  }]
@@ -291,8 +300,9 @@ Remember that:
291
300
 
292
301
  * __`from/target`__: `<element matchers>` Depending of the rule to which the options are for, the rule will be applied only if the file being analized matches with this element matcher (`from`), or the dependency being imported matches with this element matcher (`target`).
293
302
  * __`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.
303
+ * __`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.
294
304
 
295
- > Tip: All properties can receive a single matcher, or an array of matchers.
305
+ > Tip: Properties `from/target` and `disallow/allow` can receive a single matcher, or an array of matchers.
296
306
 
297
307
  ##### Elements matchers
298
308
 
@@ -303,6 +313,29 @@ Elements matchers used in the rules options can have the next formats:
303
313
  * `["helpers", { category: "data", elementName: "parsers"}]`: Will only match with helpers with category "data" and elementName "parsers" (`helpers/data/parsers.js`).
304
314
  * `["helpers", { category: "data" }]`: Will match with all helpers with category "data" (`helpers/data/*.js`)
305
315
 
316
+ ##### Error messages
317
+
318
+ The plugin returns a different default message for each rule, check the documentation of each one for further info. But some rules support defining custom messages in their configuration, as seen in ["Main format of rules options"](#main-format-of-rules-options).
319
+
320
+ When defining custom messages, it is possible to provide information about the current file or dependency. Use `${file.PROPERTY}` or `${dependency.PROPERTY}`, and it will be replaced by the correspondent captured value from the file or the dependency:
321
+
322
+ ```jsonc
323
+ {
324
+ "message": "${file.type}s of category ${file.category} are not allowed to import ${dependency.category}s"
325
+ // If the error was produced by a file with type "component" and captured value "category" being "atom", trying to import a dependency with category "molecule", the message would be:
326
+ // "components of category atom are not allowed to import molecules"
327
+ }
328
+ ```
329
+
330
+ Available properties in error templates both from `file` or `dependency` are:
331
+
332
+ * `type`: Element's type.
333
+ * `internalPath`: File path being analyzed or imported. Relative to the element's root path.
334
+ * ...All captured properties are also available
335
+
336
+
337
+ > Tip: Read ["Global settings"](#global-settings) for further info about how to capture values from elements.
338
+
306
339
  ##### Advanced example of a rule configuration
307
340
 
308
341
  Just to illustrate the high level of customization that the plugin supports, here is an example of advanced options for the `boundaries/element-types` rule based on the previous global `elements` settings example:
@@ -343,7 +376,9 @@ Just to illustrate the high level of customization that the plugin supports, her
343
376
  "disallow": [
344
377
  // disallow importing helpers with captured category "data"
345
378
  ["helpers", { "category": "data" }]
346
- ]
379
+ ],
380
+ // Custom message only for this specific error
381
+ "message": "Atom components can't import data helpers"
347
382
  },
348
383
  {
349
384
  // when file is inside a module
@@ -360,7 +395,9 @@ Just to illustrate the high level of customization that the plugin supports, her
360
395
  "disallow": [
361
396
  // disallow importing any type of component not being of family layout
362
397
  ["components", { "family": "!layout" }]
363
- ]
398
+ ],
399
+ // Custom message only for this specific error
400
+ "message": "Modules with name starting by 'page-' can't import not layout components. You tried to import a component of family ${dependency.family} from a module with name ${file.elementName}"
364
401
  }
365
402
  ]
366
403
  }]
@@ -456,8 +493,6 @@ MIT, see [LICENSE](./LICENSE) for details.
456
493
  [license-url]: https://github.com/javierbrea/eslint-plugin-boundaries/blob/master/LICENSE
457
494
  [npm-downloads-image]: https://img.shields.io/npm/dm/eslint-plugin-boundaries.svg
458
495
  [npm-downloads-url]: https://www.npmjs.com/package/eslint-plugin-boundaries
459
- [npm-dependencies-image]: https://img.shields.io/david/javierbrea/eslint-plugin-boundaries.svg
460
- [npm-dependencies-url]: https://david-dm.org/javierbrea/eslint-plugin-boundaries
461
496
  [quality-gate-image]: https://sonarcloud.io/api/project_badges/measure?project=javierbrea_eslint-plugin-boundaries&metric=alert_status
462
497
  [quality-gate-url]: https://sonarcloud.io/dashboard?id=javierbrea_eslint-plugin-boundaries
463
498
  [release-image]: https://img.shields.io/github/release-date/javierbrea/eslint-plugin-boundaries.svg
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-boundaries",
3
- "version": "2.4.2",
3
+ "version": "2.7.0",
4
4
  "description": "Eslint plugin checking architecture boundaries between elements",
5
5
  "keywords": [
6
6
  "eslint",
@@ -28,25 +28,25 @@
28
28
  "prepare": "is-ci || husky install"
29
29
  },
30
30
  "peerDependencies": {
31
- "eslint": "^6.0.0 || ^7.0.0"
31
+ "eslint": ">=6.0.0"
32
32
  },
33
33
  "dependencies": {
34
34
  "eslint-import-resolver-node": "0.3.6",
35
- "eslint-module-utils": "2.6.2",
35
+ "eslint-module-utils": "2.7.1",
36
36
  "chalk": "4.1.2",
37
- "is-core-module": "2.5.0",
37
+ "is-core-module": "2.8.0",
38
38
  "micromatch": "4.0.4"
39
39
  },
40
40
  "devDependencies": {
41
41
  "cross-env": "7.0.3",
42
- "eslint": "7.32.0",
42
+ "eslint": "8.3.0",
43
43
  "eslint-config-prettier": "8.3.0",
44
- "eslint-plugin-prettier": "3.4.0",
45
- "husky": "7.0.1",
46
- "is-ci": "3.0.0",
47
- "jest": "27.0.6",
48
- "lint-staged": "11.1.2",
49
- "prettier": "2.3.2"
44
+ "eslint-plugin-prettier": "4.0.0",
45
+ "husky": "7.0.4",
46
+ "is-ci": "3.0.1",
47
+ "jest": "27.3.1",
48
+ "lint-staged": "12.1.2",
49
+ "prettier": "2.4.1"
50
50
  },
51
51
  "lint-staged": {
52
52
  "test/**/*.js": "eslint",
@@ -54,6 +54,6 @@
54
54
  "*.js": "eslint"
55
55
  },
56
56
  "engines": {
57
- "node": "10.x || 12.x || 14.x || 15.x || 16.x"
57
+ "node": ">=12.0.0"
58
58
  }
59
59
  }
package/src/core/cache.js CHANGED
@@ -1,5 +1,3 @@
1
- const { debug } = require("../helpers/debug");
2
-
3
1
  class Cache {
4
2
  constructor(name, settings) {
5
3
  this._cache = {};
@@ -13,7 +11,6 @@ class Cache {
13
11
 
14
12
  load(key) {
15
13
  if (this._cache[key]) {
16
- debug(`Returning "${key}" ${this._name} info from cache`);
17
14
  return this._cache[key];
18
15
  }
19
16
 
@@ -32,7 +29,6 @@ class CachesManager {
32
29
  return cacheCandidate._settings === settings;
33
30
  });
34
31
  if (!cache) {
35
- debug(`${this._name} cache not found, creating new`);
36
32
  cache = new Cache(this._name, settings);
37
33
  this._caches.push(cache);
38
34
  }
@@ -5,6 +5,7 @@ const resolve = require("eslint-module-utils/resolve").default;
5
5
  const { IGNORE, INCLUDE, VALID_MODES } = require("../constants/settings");
6
6
  const { getElements } = require("../helpers/settings");
7
7
  const { debugFileInfo } = require("../helpers/debug");
8
+ const { isArray } = require("../helpers/utils");
8
9
 
9
10
  const { filesCache, importsCache, elementsCache } = require("./cache");
10
11
 
@@ -112,9 +113,7 @@ function elementTypeAndParents(path, settings) {
112
113
  let elementFound = false;
113
114
  getElements(settings).forEach((element) => {
114
115
  const typeOfMatch = VALID_MODES.includes(element.mode) ? element.mode : VALID_MODES[0];
115
- const elementPatterns = Array.isArray(element.pattern)
116
- ? element.pattern
117
- : [element.pattern];
116
+ const elementPatterns = isArray(element.pattern) ? element.pattern : [element.pattern];
118
117
  elementPatterns.forEach((elementPattern) => {
119
118
  if (!elementFound) {
120
119
  const useFullPathMatch = typeOfMatch === VALID_MODES[2] && !elementResult.type;
@@ -0,0 +1,117 @@
1
+ const { isString, isArray, replaceObjectValuesInTemplates } = require("./utils");
2
+ const { micromatchPatternReplacingObjectValues } = require("./rules");
3
+
4
+ function quote(str) {
5
+ return `'${str}'`;
6
+ }
7
+
8
+ function typeMessage(elementMatcher) {
9
+ return `elements of type ${quote(elementMatcher)}`;
10
+ }
11
+
12
+ function propertiesConcater(properties, index) {
13
+ if (properties.length > 1 && index === properties.length - 1) {
14
+ return " and";
15
+ }
16
+ if (index === 0) {
17
+ return " with";
18
+ }
19
+ return ",";
20
+ }
21
+
22
+ function micromatchPatternMessage(micromatchPatterns, elementCapturedValues) {
23
+ const micromatchPatternsWithValues = micromatchPatternReplacingObjectValues(
24
+ micromatchPatterns,
25
+ elementCapturedValues
26
+ );
27
+ if (isArray(micromatchPatternsWithValues)) {
28
+ if (micromatchPatternsWithValues.length === 1) {
29
+ return quote(micromatchPatternsWithValues[0]);
30
+ }
31
+ return micromatchPatternsWithValues.reduce((message, micromatchPattern, index) => {
32
+ if (index === 0) {
33
+ return quote(micromatchPattern);
34
+ }
35
+ if (index === micromatchPatternsWithValues.length - 1) {
36
+ return `${message} or ${quote(micromatchPattern)}`;
37
+ }
38
+ return `${message}, ${quote(micromatchPattern)}`;
39
+ }, "");
40
+ }
41
+ return quote(micromatchPatternsWithValues);
42
+ }
43
+
44
+ function capturedValuesMatcherMessage(capturedValuesPattern, elementCapturedValues) {
45
+ const capturedValuesPatternKeys = Object.keys(capturedValuesPattern);
46
+ return capturedValuesPatternKeys
47
+ .map((key) => {
48
+ return [key, capturedValuesPattern[key]];
49
+ })
50
+ .reduce((message, propertyNameAndMatcher, index) => {
51
+ return `${message}${propertiesConcater(capturedValuesPatternKeys, index)} ${
52
+ propertyNameAndMatcher[0]
53
+ } ${micromatchPatternMessage(propertyNameAndMatcher[1], elementCapturedValues)}`;
54
+ }, "");
55
+ }
56
+
57
+ function elementMatcherMessage(elementMatcher, elementCapturedValues) {
58
+ if (isString(elementMatcher)) {
59
+ return typeMessage(elementMatcher);
60
+ }
61
+ return `${typeMessage(elementMatcher[0])}${capturedValuesMatcherMessage(
62
+ elementMatcher[1],
63
+ elementCapturedValues
64
+ )}`;
65
+ }
66
+
67
+ function ruleElementMessage(elementPatterns, elementCapturedValues) {
68
+ if (isArray(elementPatterns)) {
69
+ if (elementPatterns.length === 1) {
70
+ return elementMatcherMessage(elementPatterns[0], elementCapturedValues);
71
+ }
72
+ return elementPatterns.reduce((message, elementPattern, index) => {
73
+ if (index === 0) {
74
+ return elementMatcherMessage(elementPattern, elementCapturedValues);
75
+ }
76
+ return `${message}, or ${elementMatcherMessage(elementPattern, elementCapturedValues)}`;
77
+ }, "");
78
+ }
79
+ return elementMatcherMessage(elementPatterns, elementCapturedValues);
80
+ }
81
+
82
+ function customErrorMessage(message, file, dependency) {
83
+ return replaceObjectValuesInTemplates(
84
+ replaceObjectValuesInTemplates(message, { ...file.capturedValues, type: file.type }, "file"),
85
+ { ...dependency.capturedValues, type: dependency.type, internalPath: dependency.internalPath },
86
+ "dependency"
87
+ );
88
+ }
89
+
90
+ function elementCapturedValuesMessage(capturedValues) {
91
+ if (!capturedValues) {
92
+ return "";
93
+ }
94
+ const capturedValuesKeys = Object.keys(capturedValues);
95
+ return capturedValuesKeys
96
+ .map((key) => {
97
+ return [key, capturedValues[key]];
98
+ })
99
+ .reduce((message, propertyNameAndValue, index) => {
100
+ return `${message}${propertiesConcater(capturedValuesKeys, index)} ${
101
+ propertyNameAndValue[0]
102
+ } ${quote(propertyNameAndValue[1])}`;
103
+ }, "");
104
+ }
105
+
106
+ function elementMessage(elementInfo) {
107
+ return `of type ${quote(elementInfo.type)}${elementCapturedValuesMessage(
108
+ elementInfo.capturedValues
109
+ )}`;
110
+ }
111
+
112
+ module.exports = {
113
+ quote,
114
+ ruleElementMessage,
115
+ customErrorMessage,
116
+ elementMessage,
117
+ };
@@ -1,9 +1,15 @@
1
1
  const micromatch = require("micromatch");
2
2
 
3
+ const { isArray, replaceObjectValuesInTemplates } = require("./utils");
4
+
3
5
  const REPO_URL = "https://github.com/javierbrea/eslint-plugin-boundaries";
4
6
 
7
+ function removePluginNamespace(ruleName) {
8
+ return ruleName.replace("boundaries/", "");
9
+ }
10
+
5
11
  function docsUrl(ruleName) {
6
- return `${REPO_URL}/blob/master/docs/rules/${ruleName}.md`;
12
+ return `${REPO_URL}/blob/master/docs/rules/${removePluginNamespace(ruleName)}.md`;
7
13
  }
8
14
 
9
15
  function meta({ description, schema = [], ruleName }) {
@@ -39,9 +45,7 @@ function dependencyLocation(node, context) {
39
45
  }
40
46
 
41
47
  function micromatchPatternReplacingObjectValues(pattern, object) {
42
- return Object.keys(object).reduce((result, objectKey) => {
43
- return result.replace(`\${${objectKey}}`, object[objectKey]);
44
- }, pattern);
48
+ return replaceObjectValuesInTemplates(pattern, object);
45
49
  }
46
50
 
47
51
  function isObjectMatch(objectWithMatchers, object, objectWithValuesToReplace) {
@@ -65,10 +69,10 @@ function rulesMainKey(key) {
65
69
 
66
70
  function ruleMatch(ruleMatchers, elementInfo, isMatch, elementToCompare = {}) {
67
71
  let match = { result: false, report: null };
68
- const matchers = !Array.isArray(ruleMatchers) ? [ruleMatchers] : ruleMatchers;
72
+ const matchers = !isArray(ruleMatchers) ? [ruleMatchers] : ruleMatchers;
69
73
  matchers.forEach((matcher) => {
70
74
  if (!match.result) {
71
- if (Array.isArray(matcher)) {
75
+ if (isArray(matcher)) {
72
76
  const [value, captures] = matcher;
73
77
  match = isMatch(elementInfo, value, captures, elementToCompare.capturedValues);
74
78
  } else {
@@ -106,9 +110,16 @@ function getElementRules(elementInfo, options, mainKey) {
106
110
  return [];
107
111
  }
108
112
  const key = rulesMainKey(mainKey);
109
- return options.rules.filter((rule) => {
110
- return ruleMatch(rule[key], elementInfo, isMatchElementType).result;
111
- });
113
+ return options.rules
114
+ .map((rule, index) => {
115
+ return {
116
+ ...rule,
117
+ index,
118
+ };
119
+ })
120
+ .filter((rule) => {
121
+ return ruleMatch(rule[key], elementInfo, isMatchElementType).result;
122
+ });
112
123
  }
113
124
 
114
125
  function elementRulesAllowDependency({
@@ -118,12 +129,21 @@ function elementRulesAllowDependency({
118
129
  isMatch,
119
130
  rulesMainKey: mainKey,
120
131
  }) {
121
- const [result, report] = getElementRules(element, options, mainKey).reduce(
132
+ const [result, report, ruleReport] = getElementRules(element, options, mainKey).reduce(
122
133
  (allowed, rule) => {
123
134
  if (rule.disallow) {
124
135
  const match = ruleMatch(rule.disallow, dependency, isMatch, element);
125
136
  if (match.result) {
126
- return [false, match.report];
137
+ return [
138
+ false,
139
+ match.report,
140
+ {
141
+ element: rule[rulesMainKey(mainKey)],
142
+ disallow: rule.disallow,
143
+ index: rule.index,
144
+ message: rule.message || options.message,
145
+ },
146
+ ];
127
147
  }
128
148
  }
129
149
  if (rule.allow) {
@@ -134,11 +154,19 @@ function elementRulesAllowDependency({
134
154
  }
135
155
  return allowed;
136
156
  },
137
- [options.default === "allow", null]
157
+ [
158
+ options.default === "allow",
159
+ null,
160
+ {
161
+ isDefault: true,
162
+ message: options.message,
163
+ },
164
+ ]
138
165
  );
139
166
  return {
140
167
  result,
141
168
  report,
169
+ ruleReport,
142
170
  };
143
171
  }
144
172
 
@@ -151,4 +179,5 @@ module.exports = {
151
179
  elementRulesAllowDependency,
152
180
  getElementRules,
153
181
  rulesMainKey,
182
+ micromatchPatternReplacingObjectValues,
154
183
  };
@@ -1,7 +1,8 @@
1
1
  const { TYPES, ELEMENTS, VALID_MODES } = require("../constants/settings");
2
+ const { isString } = require("./utils");
2
3
 
3
4
  function isLegacyType(type) {
4
- return typeof type === "string";
5
+ return isString(type);
5
6
  }
6
7
 
7
8
  // TODO, remove in next major version
@@ -0,0 +1,30 @@
1
+ function isString(object) {
2
+ return typeof object === "string";
3
+ }
4
+
5
+ function isArray(object) {
6
+ return Array.isArray(object);
7
+ }
8
+
9
+ function replaceObjectValueInTemplate(template, key, value, namespace) {
10
+ const keyToReplace = namespace ? `${namespace}.${key}` : key;
11
+ return template.replace(`\${${keyToReplace}}`, value);
12
+ }
13
+
14
+ function replaceObjectValuesInTemplates(strings, object, namespace) {
15
+ return Object.keys(object).reduce((result, objectKey) => {
16
+ // If template is an array, replace key by value in all patterns
17
+ if (isArray(result)) {
18
+ return result.map((resultEntry) => {
19
+ return replaceObjectValueInTemplate(resultEntry, objectKey, object[objectKey], namespace);
20
+ });
21
+ }
22
+ return replaceObjectValueInTemplate(result, objectKey, object[objectKey], namespace);
23
+ }, strings);
24
+ }
25
+
26
+ module.exports = {
27
+ isString,
28
+ isArray,
29
+ replaceObjectValuesInTemplates,
30
+ };
@@ -5,6 +5,7 @@ const { TYPES, ALIAS, ELEMENTS, VALID_MODES } = require("../constants/settings")
5
5
  const { getElementsTypeNames, isLegacyType } = require("./settings");
6
6
  const { rulesMainKey } = require("./rules");
7
7
  const { warnOnce } = require("./debug");
8
+ const { isArray, isString } = require("./utils");
8
9
 
9
10
  const invalidMatchers = [];
10
11
 
@@ -41,9 +42,9 @@ function elementsMatcherSchema(matcherOptions = DEFAULT_MATCHER_OPTIONS) {
41
42
  };
42
43
  }
43
44
 
44
- function rulesOptionsSchema(options = {}) {
45
+ function rulesOptionsSchema(options) {
45
46
  const mainKey = rulesMainKey(options.rulesMainKey);
46
- return [
47
+ const schema = [
47
48
  {
48
49
  type: "object",
49
50
  properties: {
@@ -78,15 +79,26 @@ function rulesOptionsSchema(options = {}) {
78
79
  additionalProperties: false,
79
80
  },
80
81
  ];
82
+ if (options.customMessage) {
83
+ schema[0].properties.message = {
84
+ type: "string",
85
+ };
86
+ }
87
+ if (options.customRuleMessage) {
88
+ schema[0].properties.rules.items.properties.message = {
89
+ type: "string",
90
+ };
91
+ }
92
+ return schema;
81
93
  }
82
94
 
83
95
  function isValidElementTypesMatcher(matcher, settings) {
84
- const mathcherToCheck = Array.isArray(matcher) ? matcher[0] : matcher;
96
+ const mathcherToCheck = isArray(matcher) ? matcher[0] : matcher;
85
97
  return !matcher || micromatch.some(getElementsTypeNames(settings), mathcherToCheck);
86
98
  }
87
99
 
88
100
  function validateElementTypesMatcher(elementsMatcher, settings) {
89
- const [matcher] = Array.isArray(elementsMatcher) ? elementsMatcher : [elementsMatcher];
101
+ const [matcher] = isArray(elementsMatcher) ? elementsMatcher : [elementsMatcher];
90
102
  if (!invalidMatchers.includes(matcher) && !isValidElementTypesMatcher(matcher, settings)) {
91
103
  invalidMatchers.push(matcher);
92
104
  warnOnce(`Option '${matcher}' does not match any element type from '${ELEMENTS}' setting`);
@@ -94,7 +106,7 @@ function validateElementTypesMatcher(elementsMatcher, settings) {
94
106
  }
95
107
 
96
108
  function validateElements(elements) {
97
- if (!elements || !Array.isArray(elements) || !elements.length) {
109
+ if (!elements || !isArray(elements) || !elements.length) {
98
110
  warnOnce(`Please provide element types using the '${ELEMENTS}' setting`);
99
111
  return;
100
112
  }
@@ -106,7 +118,7 @@ function validateElements(elements) {
106
118
  );
107
119
  } else {
108
120
  Object.keys(element).forEach(() => {
109
- if (!element.type || typeof element.type !== "string") {
121
+ if (!element.type || !isString(element.type)) {
110
122
  warnOnce(`Please provide type in '${ELEMENTS}' setting`);
111
123
  }
112
124
  if (element.mode && !VALID_MODES.includes(element.mode)) {
@@ -116,13 +128,10 @@ function validateElements(elements) {
116
128
  )}. Default value "${VALID_MODES[0]}" will be used instead`
117
129
  );
118
130
  }
119
- if (
120
- !element.pattern ||
121
- !(typeof element.pattern === "string" || Array.isArray(element.pattern))
122
- ) {
131
+ if (!element.pattern || !(isString(element.pattern) || isArray(element.pattern))) {
123
132
  warnOnce(`Please provide a valid pattern in '${ELEMENTS}' setting`);
124
133
  }
125
- if (element.capture && !Array.isArray(element.capture)) {
134
+ if (element.capture && !isArray(element.capture)) {
126
135
  warnOnce(`Invalid capture property in '${ELEMENTS}' setting`);
127
136
  }
128
137
  });
@@ -8,6 +8,7 @@ const {
8
8
  isMatchElementType,
9
9
  elementRulesAllowDependency,
10
10
  } = require("../helpers/rules");
11
+ const { customErrorMessage, ruleElementMessage, elementMessage } = require("../helpers/messages");
11
12
 
12
13
  function elementRulesAllowDependencyType(element, dependency, options) {
13
14
  return elementRulesAllowDependency({
@@ -15,28 +16,47 @@ function elementRulesAllowDependencyType(element, dependency, options) {
15
16
  dependency,
16
17
  options,
17
18
  isMatch: isMatchElementType,
18
- }).result;
19
+ });
20
+ }
21
+
22
+ function errorMessage(ruleData, file, dependency) {
23
+ const ruleReport = ruleData.ruleReport;
24
+ if (ruleReport.message) {
25
+ return customErrorMessage(ruleReport.message, file, dependency);
26
+ }
27
+ if (ruleReport.isDefault) {
28
+ return `No rule allowing this dependency was found. File is ${elementMessage(
29
+ file
30
+ )}. Dependency is ${elementMessage(dependency)}`;
31
+ }
32
+ return `Importing ${ruleElementMessage(
33
+ ruleReport.disallow,
34
+ file.capturedValues
35
+ )} is not allowed in ${ruleElementMessage(
36
+ ruleReport.element,
37
+ file.capturedValues
38
+ )}. Disallowed in rule ${ruleReport.index + 1}`;
19
39
  }
20
40
 
21
41
  module.exports = dependencyRule(
22
42
  {
23
43
  ruleName: RULE_ELEMENT_TYPES,
24
44
  description: `Check allowed dependencies between element types`,
25
- schema: rulesOptionsSchema(),
45
+ schema: rulesOptionsSchema({
46
+ customMessage: true,
47
+ customRuleMessage: true,
48
+ }),
26
49
  },
27
50
  function ({ dependency, file, node, context, options }) {
28
- if (
29
- dependency.isLocal &&
30
- !dependency.isIgnored &&
31
- dependency.type &&
32
- !dependency.isInternal &&
33
- !elementRulesAllowDependencyType(file, dependency, options)
34
- ) {
35
- context.report({
36
- message: `Usage of '${dependency.type}' is not allowed in '${file.type}'`,
37
- node: node,
38
- ...dependencyLocation(node, context),
39
- });
51
+ if (dependency.isLocal && !dependency.isIgnored && dependency.type && !dependency.isInternal) {
52
+ const ruleData = elementRulesAllowDependencyType(file, dependency, options);
53
+ if (!ruleData.result) {
54
+ context.report({
55
+ message: errorMessage(ruleData, file, dependency),
56
+ node: node,
57
+ ...dependencyLocation(node, context),
58
+ });
59
+ }
40
60
  }
41
61
  }
42
62
  );
@@ -8,6 +8,7 @@ const {
8
8
  isMatchElementKey,
9
9
  elementRulesAllowDependency,
10
10
  } = require("../helpers/rules");
11
+ const { customErrorMessage, ruleElementMessage, elementMessage } = require("../helpers/messages");
11
12
 
12
13
  function isMatchElementInternalPath(elementInfo, matcher, options) {
13
14
  return isMatchElementKey(elementInfo, matcher, options, "internalPath");
@@ -20,7 +21,23 @@ function elementRulesAllowEntryPoint(element, dependency, options) {
20
21
  options,
21
22
  isMatch: isMatchElementInternalPath,
22
23
  rulesMainKey: "target",
23
- }).result;
24
+ });
25
+ }
26
+
27
+ function errorMessage(ruleData, file, dependency) {
28
+ const ruleReport = ruleData.ruleReport;
29
+ if (ruleReport.message) {
30
+ return customErrorMessage(ruleReport.message, file, dependency);
31
+ }
32
+ if (ruleReport.isDefault) {
33
+ return `No rule allows the entry point '${
34
+ dependency.internalPath
35
+ }' in dependencies ${elementMessage(dependency)}`;
36
+ }
37
+ return `The entry point '${dependency.internalPath}' is not allowed in ${ruleElementMessage(
38
+ ruleReport.element,
39
+ dependency.capturedValues
40
+ )}. Disallowed in rule ${ruleReport.index + 1}`;
24
41
  }
25
42
 
26
43
  module.exports = dependencyRule(
@@ -29,20 +46,20 @@ module.exports = dependencyRule(
29
46
  description: `Check entry point used for each element type`,
30
47
  schema: rulesOptionsSchema({
31
48
  rulesMainKey: "target",
49
+ customMessage: true,
50
+ customRuleMessage: true,
32
51
  }),
33
52
  },
34
53
  function ({ dependency, file, node, context, options }) {
35
- if (
36
- !dependency.isIgnored &&
37
- dependency.type &&
38
- !dependency.isInternal &&
39
- !elementRulesAllowEntryPoint(file, dependency, options)
40
- ) {
41
- context.report({
42
- message: `Entry point '${dependency.internalPath}' is not allowed in '${dependency.type}'`,
43
- node: node,
44
- ...dependencyLocation(node, context),
45
- });
54
+ if (!dependency.isIgnored && dependency.type && !dependency.isInternal) {
55
+ const ruleData = elementRulesAllowEntryPoint(file, dependency, options);
56
+ if (!ruleData.result) {
57
+ context.report({
58
+ message: errorMessage(ruleData, file, dependency),
59
+ node: node,
60
+ ...dependencyLocation(node, context),
61
+ });
62
+ }
46
63
  }
47
64
  },
48
65
  {
@@ -1,4 +1,4 @@
1
- const { RULE_NO_INTERNAL } = require("../constants/settings");
1
+ const { RULE_NO_PRIVATE } = require("../constants/settings");
2
2
 
3
3
  const dependencyRule = require("../rules-factories/dependency-rule");
4
4
 
@@ -6,7 +6,7 @@ const { dependencyLocation } = require("../helpers/rules");
6
6
 
7
7
  module.exports = dependencyRule(
8
8
  {
9
- ruleName: RULE_NO_INTERNAL,
9
+ ruleName: RULE_NO_PRIVATE,
10
10
  description: `Prevent importing private elements of another element`,
11
11
  schema: [
12
12
  {
@@ -5,9 +5,9 @@ const { validateSettings, validateRules } = require("../helpers/validations");
5
5
 
6
6
  const { meta } = require("../helpers/rules");
7
7
 
8
- module.exports = function (ruleDescription, rule, ruleOptions = {}) {
8
+ module.exports = function (ruleMeta, rule, ruleOptions = {}) {
9
9
  return {
10
- ...meta(ruleDescription),
10
+ ...meta(ruleMeta),
11
11
  create: function (context) {
12
12
  const options = context.options[0];
13
13
  validateSettings(context.settings);
package/CHANGELOG.md DELETED
@@ -1,208 +0,0 @@
1
- # Change Log
2
- All notable changes to this project will be documented in this file.
3
-
4
- The format is based on [Keep a Changelog](http://keepachangelog.com/)
5
- and this project adheres to [Semantic Versioning](http://semver.org/).
6
-
7
- ## [unreleased]
8
- ### Added
9
- ### Changed
10
- ### Fixed
11
- ### Removed
12
- ### BREAKING CHANGES
13
-
14
- ## [2.4.2] - 2021-08-22
15
-
16
- ### Added
17
- - docs(#107): Add usage with TypeScript docs
18
-
19
- ### Changed
20
- - chore(deps): Update devDependencies
21
-
22
-
23
- ## [2.4.1] - 2021-08-16
24
-
25
- ### Fixed
26
- - fix: Remove trace when ESLINT_PLUGIN_BOUNDARIES_DEBUG is not set
27
-
28
- ## [2.4.0] - 2021-08-15
29
-
30
- ### Added
31
- - feat: Improve performance adding internal cache
32
-
33
- ### Changed
34
- - chore(deps): Update devDependencies
35
-
36
- ## [2.3.0] - 2021-07-20
37
-
38
- ### Added
39
- - feat(#131): Add `boundaries/include` option allowing to ignore all by default except files matching the pattern.
40
-
41
- ### Changed
42
- - feat(#132): Detect paths from any `node_modules` folder as external
43
- - chore(deps): Update devDependencies
44
-
45
- ## [2.2.0] - 2021-05-29
46
-
47
- ### Added
48
- - chore(deps): Add node.js v16.x to engines and add tests using it
49
-
50
- ### Changed
51
- - chore(deps): Update devDependencies
52
- - chore: Migrate Sonar project
53
- ## [2.1.0] - 2021-03-25
54
-
55
- ### Added
56
- - feat: Add basePattern and baseCapture options to elements settings (#97)
57
-
58
- ### Changed
59
- - chore(deps): Update dependencies
60
-
61
- ### Fixed
62
- - fix: Avoid crash when import path is broken (#96)
63
-
64
- ## [2.0.0] - 2021-02-02
65
-
66
- ### Added
67
- - feat: Support multiple levels of categorization and any type of project structure (#75)
68
- - feat: Support elements as files (#75)
69
- - feat(settings): Added support for `import/resolver` setting
70
- - feat(options): Support micromatch patterns in rules options (#11, #10)
71
- - feat: Add debug mode
72
- - test: Add more than 500 tests using different project structure examples, with different categorization levels, elements as folders, as files, etc.
73
- - test: Add one test for each rules docs example
74
- - chore: Run tests on Windows OS again (#74)
75
-
76
- ### Changed
77
- - feat(settings): Deprecated `boundaries/types` setting. `boundaries/elements` should be used instead. If it is not present, `boundaries/types` will be used as fallback
78
- - feat(rules): Rename `allowed-types` rule into `element-types` (now it can be used to allow/disallow). Change the format of rule options
79
- - feat(rules): Rename `no-external` rule into `external` (now it can be used to allow/disallow). Change the format of rule options
80
- - feat(rules): Change the format of `entry-point` rule options (now it support allow/disallow format)
81
- - feat(rules): Rename `no-import-ignored` rule into `no-ignored` (the majority of the plugin rules are referred to `import` statements, so it is not necessary to specify it in the rule name)
82
- - feat(rules): Rename `no-import-not-recognized-types` rule into `no-unknown`
83
- - feat(rules): Rename `prefer-recognized-types` rule into `no-unknown-files`
84
- - refactor(core): Use `eslint-module-utils/resolve` to get files and import paths. Use `micromatch` to match settings and options. Adapt the whole core to this new approach
85
-
86
- ### Fixed
87
- - fix: Support scoped packages in external rule (#59)
88
-
89
- ### Removed
90
- - feat(settings): Removed `boundaries/alias` setting
91
-
92
- ### BREAKING CHANGES
93
- - Removed `boundaries/alias` setting. `import/resolver` has to be used instead
94
- - Renamed `allowed-types` rule into `element-types` (now it can be used to allow/disallow). Changed the format of rule options
95
- - Changed the format of `entry-point` rule options (now it support allow/disallow format)
96
- - Renamed `no-external` rule into `external` (now it can be used to allow/disallow). Changed the format of rule options
97
- - Renamed `no-import-ignored` rule into `no-ignored` (the majority of the plugin rules are referred to `import` statements, so it is not necessary to specify it in the rule name)
98
- - Renamed `no-import-not-recognized-types` rule into `no-unknown`
99
- - Renamed `prefer-recognized-types` rule into `no-unknown-files`
100
-
101
- ## [2.0.0-beta.4] - 2021-01-30
102
-
103
- ### Added
104
- - feat: debug files and imports info when ESLINT_PLUGIN_BOUNDARIES_DEBUG environment variable exists
105
- - feat: `mode` option in `elements` setting now also accepts `full` as value. Pattern will try to match the full path in that case.
106
- - feat: support defining multiple micromatch patterns in an array in the `pattern` property of `elements` setting.
107
-
108
- ## [2.0.0-beta.3] - 2021-01-26
109
-
110
- ### Fixed
111
- - fix: node_modules packages were being recognized as local
112
-
113
- ## [2.0.0-beta.2] - 2021-01-26
114
-
115
- ### Fixed
116
- - fix: Add folder resolver-legacy-alias to npm package
117
-
118
- ## [2.0.0-beta.1] - 2021-01-26
119
-
120
- ### Added
121
- - feat: Support multiple levels of categorization and any type of project structure (#75)
122
- - feat: Support elements as files (#75)
123
- - feat(settings): Added support for `import/resolver` setting
124
- - feat(options): Support micromatch patterns in rules options (#11, #10)
125
- - test: Add more than 500 tests using different project structure examples, with different categorization levels, elements as folders, as files, etc.
126
- - test: Add one test for each rules docs example
127
-
128
- ### Changed
129
- - feat(settings): Deprecated `boundaries/types` setting. `boundaries/elements` should be used instead. If it is not present, `boundaries/types` will be used as fallback
130
- - feat(rules): Rename `allowed-types` rule into `element-types` (now it can be used to allow/disallow). Change the format of rule options
131
- - feat(rules): Rename `no-external` rule into `external` (now it can be used to allow/disallow). Change the format of rule options
132
- - feat(rules): Change the format of `entry-point` rule options (now it support allow/disallow format)
133
- - feat(rules): Rename `no-import-ignored` rule into `no-ignored` (the majority of the plugin rules are referred to `import` statements, so it is not necessary to specify it in the rule name)
134
- - feat(rules): Rename `no-import-not-recognized-types` rule into `no-unknown`
135
- - feat(rules): Rename `prefer-recognized-types` rule into `no-unknown-files`
136
- - refactor(core): Use `eslint-module-utils/resolve` to get files and import paths. Use `micromatch` to match settings and options. Adapt the whole core to this new approach
137
-
138
- ### Fixed
139
- - fix: Support scoped packages in external rule (#59)
140
-
141
- ### Removed
142
- - feat(settings): Removed `boundaries/alias` setting
143
-
144
- ### BREAKING CHANGES
145
- - Removed `boundaries/alias` setting. `import/resolver` has to be used instead
146
- - Renamed `allowed-types` rule into `element-types` (now it can be used to allow/disallow). Changed the format of rule options
147
- - Changed the format of `entry-point` rule options (now it support allow/disallow format)
148
- - Renamed `no-external` rule into `external` (now it can be used to allow/disallow). Changed the format of rule options
149
- - Renamed `no-import-ignored` rule into `no-ignored` (the majority of the plugin rules are referred to `import` statements, so it is not necessary to specify it in the rule name)
150
- - Renamed `no-import-not-recognized-types` rule into `no-unknown`
151
- - Renamed `prefer-recognized-types` rule into `no-unknown-files`
152
-
153
- ## [1.1.1] - 2020-12-11
154
- ### Added
155
- - chore(deps): Add Node.js 10.x support while it is in maintenance
156
-
157
- ### Changed
158
- - chore(ci-cd): Migrate build and publish pipelines to github actions
159
- - chore(deps): Update dependencies
160
-
161
- ### Fixed
162
- - fix(#65): Fixed error on dependency scanning when dependencyInfo.name is null (thanks to @skurfuerst)
163
-
164
- ## [1.1.0] - 2020-11-12
165
- ### Added
166
- - feat(no-external): Allow forbid importing external libraries specifiers
167
- - chore(ci-cd): Add github workflows for publishing to gpr and check package version
168
- - chore(engines): Add node v15.x to engines
169
-
170
- ### Fixed
171
- - fix(no-external): Do not allow importing subfolders of forbidden external libraries
172
-
173
- ## [1.0.2] - 2020-10-18
174
- ### Added
175
- - chore: Run tests on Windows OS in pipelines
176
-
177
- ### Fixed
178
- - fix: Plugin was not working properly on Windows
179
-
180
- ## [1.0.1] - 2020-08-27
181
- ### Changed
182
- - chore: Update dependencies
183
-
184
- ### Fixed
185
- - docs: Fix typo
186
-
187
- ## [1.0.0] - 2020-06-13
188
- ### Added
189
- - test(unit): Add unit tests
190
- - test(coverage): Increase coverage threshold to 100
191
- - feat(logs): Add chalk to warning logs
192
-
193
- ### Changed
194
- - refactor: Remove duplicated code
195
-
196
- ## [1.0.0-beta.3] - 2020-06-12
197
- ### Fixed
198
- - fix(no-external): Fix no-external rule. There was an error reading options, so it was not being applied.
199
- - fix(prefer-recognized-types): Do not apply prefer-recognized-types rule to ignored files.
200
- - fix(rules): Ignore dependencies in all rules (except no-import-ignored) if they are marked as ignored
201
-
202
- ## [1.0.0-beta.2] - 2020-06-11
203
- ### Fixed
204
- - fix(helpers): Avoid error in helper when an element is not recognized
205
-
206
- ## [1.0.0-beta.1] - 2020-06-11
207
- ### Added
208
- - First pre-release