eslint-plugin-stratified-design 0.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/.eslintrc.js +19 -0
- package/.github/workflows/release.yml +18 -0
- package/LICENSE +7 -0
- package/README.md +42 -0
- package/docs/rules/lower-level-imports.md +182 -0
- package/docs/rules/no-same-level-funcs.md +30 -0
- package/lib/helpers/lowerLevelImports/1 layer.js +220 -0
- package/lib/helpers/lowerLevelImports/2 layer.js +25 -0
- package/lib/helpers/lowerLevelImports/3 layer.js +108 -0
- package/lib/helpers/lowerLevelImports/4 layer.js +61 -0
- package/lib/helpers/lowerLevelImports/index.js +23 -0
- package/lib/index.js +22 -0
- package/lib/rules/lower-level-imports.js +165 -0
- package/lib/rules/no-same-level-funcs.js +50 -0
- package/package.json +41 -0
- package/tests/lib/rules/lower-level-imports.js +314 -0
- package/tests/lib/rules/no-same-level-funcs.js +46 -0
- package/tests-in-package/.eslintrc.js +21 -0
- package/tests-in-package/node_modules/.package-lock.json +56 -0
- package/tests-in-package/package-lock.json +74 -0
- package/tests-in-package/package.json +8 -0
- package/tests-in-package/src/layer1/module.js +3 -0
- package/tests-in-package/src/layer2/module.js +3 -0
- package/tests-in-package/src/layer3/1 lib.js +5 -0
- package/tests-in-package/src/layer3/2 lib.js +3 -0
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
root: true,
|
|
5
|
+
extends: [
|
|
6
|
+
"eslint:recommended",
|
|
7
|
+
"plugin:eslint-plugin/recommended",
|
|
8
|
+
"plugin:node/recommended",
|
|
9
|
+
],
|
|
10
|
+
env: {
|
|
11
|
+
node: true,
|
|
12
|
+
},
|
|
13
|
+
overrides: [
|
|
14
|
+
{
|
|
15
|
+
files: ["tests/**/*.js"],
|
|
16
|
+
env: { mocha: true },
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [created]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v3
|
|
12
|
+
- uses: actions/setup-node@v3
|
|
13
|
+
with:
|
|
14
|
+
node-version: 16
|
|
15
|
+
registry-url: https://registry.npmjs.org/
|
|
16
|
+
- run: npm ci
|
|
17
|
+
- run: npm test
|
|
18
|
+
- run: npm publish
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ISC License (ISCL)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Hodoug Joung
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# eslint-plugin-stratified-design
|
|
2
|
+
|
|
3
|
+
ESLint rules for stratified design, inspired by "[Grokking Simplicity](https://grokkingsimplicity.com)" written by Erick Normand, for practicing stratified design.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
First, ensure you have [ESLint](https://eslint.org/) installed:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm i eslint --save-dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Next, install `eslint-plugin-stratified-design`:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install eslint-plugin-stratified-design --save-dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
Add `stratified-design` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"plugins": ["stratified-design"]
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then configure the rules you wish to use under the rules section:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"rules": {
|
|
34
|
+
"stratified-design/rule-name": ["error"]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Supported Rules
|
|
40
|
+
|
|
41
|
+
- [lower-level-imports](https://github.com/anisotropy/eslint-plugin-stratified-design/blob/main/docs/rules/lower-level-imports.md): Requires lower-level modules to be imported.
|
|
42
|
+
- [no-same-level-funcs](https://github.com/anisotropy/eslint-plugin-stratified-design/blob/main/docs/rules/no-same-level-funcs.md): Disallows calling functions in the same file.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Require that lower-level modules be imported (lower-level-imports)
|
|
2
|
+
|
|
3
|
+
This rule enforces the requirement for importing lower-level modules. Here's an explanation:
|
|
4
|
+
|
|
5
|
+
- Functions in the same file within the same folder are considered to be at the same level.
|
|
6
|
+
- Child folders are considered to be at a lower level than the parent folder.
|
|
7
|
+
- Installed modules (node modules) are considered to be at the lowest level.
|
|
8
|
+
|
|
9
|
+
However, you can modify this hierarchy using the `structure` option.
|
|
10
|
+
|
|
11
|
+
This rule works correctly on POSIX systems, where the path segment separator is `/`. It will be updated to work well on Windows systems in the future.
|
|
12
|
+
|
|
13
|
+
### Options
|
|
14
|
+
|
|
15
|
+
The syntax to specify the level structure is as follows:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
"lower-level-imports": ["error", {
|
|
19
|
+
"structure": ["layer1", "layer2", "layer3"]
|
|
20
|
+
}]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
In the folder array, the file or folder on the left is considered to be at a higher level than the one on the right.
|
|
24
|
+
|
|
25
|
+
To designate a layer as an interface layer, set `interface` to `true`:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
"lower-level-imports": ["error", {
|
|
29
|
+
"structure": ["layer1", { "name": "layer2", "interface": true }, "layer3"],
|
|
30
|
+
}]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
For the 'interface layer,' refer to "[Grokking Simplicity](https://grokkingsimplicity.com)."
|
|
34
|
+
|
|
35
|
+
To locate a node module in the structure, set `isNodeModule` to `true`:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
"lower-level-imports": ["error", {
|
|
39
|
+
"structure": ["layer1", { "name": "nodeModule", "isNodeModule": true }, "layer3"],
|
|
40
|
+
}]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The default root directory is the current working directory. To change the root directory, use the `root` option:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
"lower-level-imports": ["error", {
|
|
47
|
+
"structure": ["layer1", "layer2", "layer3"],
|
|
48
|
+
"root": "./src"
|
|
49
|
+
}]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
If the name of an imported module has an alias, register the alias using the `aliases` option:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
"lower-level-imports": ["error", {
|
|
56
|
+
"structure": ["layer1", "layer2", "layer3"],
|
|
57
|
+
"aliases": { "@": "./src" }
|
|
58
|
+
}]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If you want to register the level of a layer by 'number,' set the option `useLevelNumber` to `true`:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
"lower-level-imports": ["error", { "useLevelNumber": true }]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The options `structure` and `useLevelNumber` can be used together.
|
|
68
|
+
|
|
69
|
+
You can register the files to apply the rule (`lower-level-imports`) using the `include` and `exclude` options:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
"lower-level-imports": ["error", { "include": ["**/*.js"], "exclude": ["**/*.test.js"] }]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The default is as follows:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"include": ["**/*.{js,ts,jsx,tsx}"],
|
|
80
|
+
"exclude": ["**/*.{spec,test}.{js,ts,jsx,tsx}"]
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Rule Details
|
|
85
|
+
|
|
86
|
+
If a file structure is as follows:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
src/
|
|
90
|
+
┣ layer1.js
|
|
91
|
+
┣ layer2/
|
|
92
|
+
┃ ┣ file.js
|
|
93
|
+
┃ ┣ otherFile.js
|
|
94
|
+
┃ ┗ subFolder/
|
|
95
|
+
┗ layer3/
|
|
96
|
+
┣ entry.js
|
|
97
|
+
┣ 1 layer.js
|
|
98
|
+
┗ 2 layer.js
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Examples of **incorrect** code for this rule:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
/* "lower-level-imports": ["error"] */
|
|
105
|
+
// ./src/layer1.js
|
|
106
|
+
import { func } from "../layer2/file";
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
/* "lower-level-imports": ["error"] */
|
|
111
|
+
// ./src/layer2/file.js
|
|
112
|
+
import { func } from "./otherFile";
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
/* "lower-level-imports": ["error", { "structure": ["layer1", "layer2"] }] */
|
|
117
|
+
// ./src/layer2/file.js
|
|
118
|
+
import { func } from "../layer1";
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
/* "lower-level-imports": ["error", {
|
|
123
|
+
"structure": ["layer1", { name: "layer2", interface: true }, "layer3"]
|
|
124
|
+
}] */
|
|
125
|
+
// ./src/layer1.js
|
|
126
|
+
import { func } from "../layer3/entry";
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
/* "lower-level-imports": ["error", {
|
|
131
|
+
"structure": ["layer1", { "name": "nodeModule", "isNodeModule": true }, "layer3"],
|
|
132
|
+
}] */
|
|
133
|
+
// ./src/layer3/entry.js
|
|
134
|
+
import { func } from "nodeModule";
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
/* "lower-level-imports": ["error", { "useLevelNumber": true }}] */
|
|
139
|
+
// ./src/layer1.js
|
|
140
|
+
import { func } from "layer3/1 layer";
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Examples of **correct** code for this rule:
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
/* "lower-level-imports": ["error"] */
|
|
147
|
+
// ./src/layer2/file.js
|
|
148
|
+
import { func } from "./subFolder/file";
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
/* "lower-level-imports": ["error", { "structure": ["layer1", "layer2"] }] */
|
|
153
|
+
// ./src/layer1.js
|
|
154
|
+
import { func } from "../layer2/file";
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
/* "lower-level-imports": ["error", {
|
|
159
|
+
"structure": ["layer1", "layer2"],
|
|
160
|
+
"alias": { "@/": "./src/" }
|
|
161
|
+
}] */
|
|
162
|
+
// ./src/layer1.js
|
|
163
|
+
import { func } from "@/layer2/file";
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
/* "lower-level-imports": ["error", { "useLevelNumber": true }] */
|
|
168
|
+
// ./src/layer3/1 layer.js
|
|
169
|
+
import { func } from "./2 layer";
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
/* "lower-level-imports": ["error", { "useLevelNumber": true }] */
|
|
174
|
+
// ./src/layer3/entry.js
|
|
175
|
+
import { func } from "./1 layer";
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
/* "lower-level-imports": ["error", { "structure": ["layer1", "layer3"], "useLevelNumber": true }] */
|
|
180
|
+
// ./src/layer.js
|
|
181
|
+
import { func } from "../layer3/entry";
|
|
182
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Disallow calling functions in the same file (no-same-level-funcs)
|
|
2
|
+
|
|
3
|
+
This rule prohibits calling functions at the same level in the same file.
|
|
4
|
+
|
|
5
|
+
## Rule Details
|
|
6
|
+
|
|
7
|
+
Examples of **incorrect** code for this rule:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
function func1(...) {
|
|
11
|
+
...
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const func2(...) => { ... }
|
|
15
|
+
|
|
16
|
+
function func3(...) {
|
|
17
|
+
func1(...);
|
|
18
|
+
func2(...);
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Examples of **correct** code for this rule:
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
function func1(...) {
|
|
26
|
+
const func2(...) => { ... };
|
|
27
|
+
...
|
|
28
|
+
func2(...);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
const { minimatch } = require("minimatch");
|
|
2
|
+
const { report: reportError } = require("./2 layer");
|
|
3
|
+
const {
|
|
4
|
+
isNodeModule,
|
|
5
|
+
findLevel: findLayerLevel,
|
|
6
|
+
hasInterface: hasInterfaceBetween,
|
|
7
|
+
removeAlias: removeAliasFromModuleSource,
|
|
8
|
+
} = require("./3 layer");
|
|
9
|
+
const {
|
|
10
|
+
toRelative,
|
|
11
|
+
toSegments,
|
|
12
|
+
toPath,
|
|
13
|
+
resolvePath,
|
|
14
|
+
parsePath,
|
|
15
|
+
} = require("./4 layer");
|
|
16
|
+
|
|
17
|
+
const FINISHED = "finished";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
*
|
|
21
|
+
* @param {string} cwd
|
|
22
|
+
* @param {*} options
|
|
23
|
+
* @returns
|
|
24
|
+
*/
|
|
25
|
+
const createRootDir = (cwd, options) => {
|
|
26
|
+
return resolvePath(cwd, options.root);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {*} options
|
|
31
|
+
* @param {string} contextFileSource
|
|
32
|
+
*/
|
|
33
|
+
const parseFileSource = (options, contextFileSource) => {
|
|
34
|
+
const fileSource = resolvePath(contextFileSource);
|
|
35
|
+
const parsedFileSource = parsePath(fileSource);
|
|
36
|
+
const fileDir = resolvePath(parsedFileSource.dir);
|
|
37
|
+
const filePath = resolvePath(fileDir, parsedFileSource.name);
|
|
38
|
+
const isIncludedFile = options.include.find((pattern) =>
|
|
39
|
+
minimatch(fileSource, pattern)
|
|
40
|
+
);
|
|
41
|
+
const isExcludedFile = options.exclude.find((pattern) =>
|
|
42
|
+
minimatch(fileSource, pattern)
|
|
43
|
+
);
|
|
44
|
+
return {
|
|
45
|
+
fileDir,
|
|
46
|
+
filePath,
|
|
47
|
+
isExcludedFile: !isIncludedFile || isExcludedFile,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} cwd
|
|
53
|
+
* @param {string} fileDir
|
|
54
|
+
* @param {{alias: string, path: string}[]} aliases
|
|
55
|
+
*/
|
|
56
|
+
const createModulePath = (cwd, fileDir, aliases) => {
|
|
57
|
+
const removeAlias = removeAliasFromModuleSource(cwd, fileDir, aliases);
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} moduleSourceWithAlias
|
|
60
|
+
*/
|
|
61
|
+
return (moduleSourceWithAlias) => {
|
|
62
|
+
const moduleSource = removeAlias(moduleSourceWithAlias);
|
|
63
|
+
const isNodeModule = moduleSource.startsWith(".") === false;
|
|
64
|
+
return isNodeModule ? moduleSource : resolvePath(fileDir, moduleSource);
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Report error about using `options.useLevelNumber`
|
|
70
|
+
* @param {import('eslint').Rule.RuleContext} context
|
|
71
|
+
* @param {*} options
|
|
72
|
+
* @param {string} rootDir
|
|
73
|
+
* @param {string} filePath
|
|
74
|
+
*/
|
|
75
|
+
const reportHasProperLevelNumber = (context, options, rootDir, filePath) => {
|
|
76
|
+
/**
|
|
77
|
+
* @param {import('eslint').Rule.NodeParentExtension} node
|
|
78
|
+
* @param {string} modulePath
|
|
79
|
+
*/
|
|
80
|
+
return (node, modulePath) => {
|
|
81
|
+
if (!options.useLevelNumber) return;
|
|
82
|
+
|
|
83
|
+
const report = (messageId) =>
|
|
84
|
+
reportError(context, rootDir, filePath)(node, messageId, modulePath);
|
|
85
|
+
|
|
86
|
+
const extractLevel = (segment) => {
|
|
87
|
+
const level = segment.split(" ")[0];
|
|
88
|
+
return /^[\d]+$/.test(level) ? Number(level) : null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const parentPath = (() => {
|
|
92
|
+
const moduleSegment = toSegments(modulePath);
|
|
93
|
+
const fileSegment = toSegments(filePath);
|
|
94
|
+
const parent = moduleSegment.reduce((parent, seg, index) => {
|
|
95
|
+
if (fileSegment[index] === seg) parent.push(seg);
|
|
96
|
+
return parent;
|
|
97
|
+
}, []);
|
|
98
|
+
return toPath(parent);
|
|
99
|
+
})();
|
|
100
|
+
|
|
101
|
+
const { moduleLevel, isInterfaceError } = (() => {
|
|
102
|
+
const segments = toSegments(toRelative(parentPath, modulePath));
|
|
103
|
+
const level = extractLevel(segments[1]);
|
|
104
|
+
return {
|
|
105
|
+
moduleLevel: level,
|
|
106
|
+
isInterfaceError: segments.length > 2,
|
|
107
|
+
};
|
|
108
|
+
})();
|
|
109
|
+
|
|
110
|
+
if (isInterfaceError) {
|
|
111
|
+
report("interface");
|
|
112
|
+
return FINISHED;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (moduleLevel === null) return;
|
|
116
|
+
|
|
117
|
+
const fileLevel = (() => {
|
|
118
|
+
const segments = toSegments(toRelative(parentPath, filePath));
|
|
119
|
+
const level = extractLevel(segments[1]);
|
|
120
|
+
if (level === null && segments.length === 2 && segments[0] == ".")
|
|
121
|
+
return -1;
|
|
122
|
+
return level;
|
|
123
|
+
})();
|
|
124
|
+
|
|
125
|
+
if (fileLevel === null) {
|
|
126
|
+
report("interface");
|
|
127
|
+
return FINISHED;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (fileLevel >= moduleLevel) {
|
|
131
|
+
report("not-lower-level");
|
|
132
|
+
return FINISHED;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return FINISHED;
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {string} fileDir
|
|
141
|
+
* @return return `FINISHED` if the imported module(`modulePath`) is in a sub directory of the file directory(`fileDir`)
|
|
142
|
+
*/
|
|
143
|
+
const reportInSubDirOfFileDir = (fileDir) => {
|
|
144
|
+
/**
|
|
145
|
+
* @param {import('eslint').Rule.NodeParentExtension} node
|
|
146
|
+
* @param {string} modulePath
|
|
147
|
+
*/
|
|
148
|
+
return (node, modulePath) => {
|
|
149
|
+
const relModulePath = toRelative(fileDir, modulePath);
|
|
150
|
+
if (
|
|
151
|
+
relModulePath.startsWith("..") === false &&
|
|
152
|
+
toSegments(relModulePath).length >= 3
|
|
153
|
+
) {
|
|
154
|
+
return FINISHED;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Report error about using `options.structure`
|
|
161
|
+
* @param {import('eslint').Rule.RuleContext} context
|
|
162
|
+
* @param {{[string]: string | {name: string, isNodeModule: boolean, interface: boolean}}} structure
|
|
163
|
+
* @param {string} rootDir
|
|
164
|
+
* @param {number | null} fileLevel
|
|
165
|
+
* @param {string} filePath
|
|
166
|
+
*/
|
|
167
|
+
const reportHasProperLevel = (
|
|
168
|
+
context,
|
|
169
|
+
structure,
|
|
170
|
+
rootDir,
|
|
171
|
+
fileLevel,
|
|
172
|
+
filePath
|
|
173
|
+
) => {
|
|
174
|
+
const findLevel = findLayerLevel(structure);
|
|
175
|
+
const hasInterface = hasInterfaceBetween(structure, fileLevel);
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {import('eslint').Rule.NodeParentExtension} node
|
|
179
|
+
* @param {string} modulePath
|
|
180
|
+
*/
|
|
181
|
+
return (node, modulePath) => {
|
|
182
|
+
const report = (messageId) =>
|
|
183
|
+
reportError(context, rootDir, filePath)(node, messageId, modulePath);
|
|
184
|
+
|
|
185
|
+
const isNodeModulePath = isNodeModule(rootDir)(modulePath);
|
|
186
|
+
|
|
187
|
+
if (fileLevel === null) {
|
|
188
|
+
if (!isNodeModulePath) report("not-registered:file");
|
|
189
|
+
return FINISHED;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const moduleLevel = findLevel(modulePath);
|
|
193
|
+
if (moduleLevel === null) {
|
|
194
|
+
if (!isNodeModulePath) report("not-registered:module");
|
|
195
|
+
return FINISHED;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (fileLevel >= moduleLevel) {
|
|
199
|
+
report("not-lower-level");
|
|
200
|
+
return FINISHED;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (hasInterface(moduleLevel)) {
|
|
204
|
+
report("interface");
|
|
205
|
+
return FINISHED;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return FINISHED;
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
module.exports = {
|
|
213
|
+
FINISHED,
|
|
214
|
+
createRootDir,
|
|
215
|
+
parseFileSource,
|
|
216
|
+
createModulePath,
|
|
217
|
+
reportHasProperLevelNumber,
|
|
218
|
+
reportInSubDirOfFileDir,
|
|
219
|
+
reportHasProperLevel,
|
|
220
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const { isNodeModule } = require("./3 layer");
|
|
2
|
+
const { toRelative } = require("./4 layer");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Report eslint error
|
|
6
|
+
* @param {import('eslint').Rule.RuleContext} context
|
|
7
|
+
* @param {string} rootDir
|
|
8
|
+
* * @param {string} filePath
|
|
9
|
+
*/
|
|
10
|
+
const report = (context, rootDir, filePath) => {
|
|
11
|
+
/**
|
|
12
|
+
* @param {import('eslint').Rule.NodeParentExtension} node
|
|
13
|
+
* @param {string} messageId
|
|
14
|
+
* @param {string} modulePath
|
|
15
|
+
*/
|
|
16
|
+
return (node, messageId, modulePath) => {
|
|
17
|
+
const module = isNodeModule(rootDir)(modulePath)
|
|
18
|
+
? modulePath
|
|
19
|
+
: toRelative(rootDir, modulePath);
|
|
20
|
+
const file = toRelative(rootDir, filePath);
|
|
21
|
+
context.report({ node, messageId, data: { module, file } });
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
module.exports = { report };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const {
|
|
2
|
+
toSegments,
|
|
3
|
+
toPath,
|
|
4
|
+
joinPath,
|
|
5
|
+
resolvePath,
|
|
6
|
+
toRelative,
|
|
7
|
+
} = require("./4 layer");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {*} options
|
|
11
|
+
* @param {string} rootDir
|
|
12
|
+
*/
|
|
13
|
+
const createStructure = (options, rootDir) => {
|
|
14
|
+
return options.structure.map((layer) => {
|
|
15
|
+
const theLayer = typeof layer === "string" ? { name: layer } : layer;
|
|
16
|
+
return theLayer.isNodeModule
|
|
17
|
+
? theLayer
|
|
18
|
+
: { ...theLayer, name: joinPath(rootDir, theLayer.name) };
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {*} options
|
|
24
|
+
*/
|
|
25
|
+
const createAliases = (options) => {
|
|
26
|
+
return Object.keys(options.aliases)
|
|
27
|
+
.sort((a, b) => b.length - a.length)
|
|
28
|
+
.map((alias) => ({ alias, path: options.aliases[alias] }));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Replace an alias into the corresponding path
|
|
33
|
+
* @param {string} cwd
|
|
34
|
+
* @param {string} fileDir
|
|
35
|
+
* @param {{alias: string, path: string}[]} aliases
|
|
36
|
+
*/
|
|
37
|
+
const removeAlias = (cwd, fileDir, aliases) => {
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} moduleSource
|
|
40
|
+
*/
|
|
41
|
+
return (moduleSource) => {
|
|
42
|
+
const { alias, path } =
|
|
43
|
+
aliases.find(({ alias }) => moduleSource.startsWith(alias)) || {};
|
|
44
|
+
if (!alias) return moduleSource;
|
|
45
|
+
const modulePath = resolvePath(cwd, moduleSource.replace(alias, path));
|
|
46
|
+
return toRelative(fileDir, modulePath);
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if a module is node module
|
|
52
|
+
* @param {string} rootDir
|
|
53
|
+
* @returns `true` if `modulePath` is node module path
|
|
54
|
+
*/
|
|
55
|
+
const isNodeModule = (rootDir) => {
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} modulePath
|
|
58
|
+
*/
|
|
59
|
+
return (modulePath) => {
|
|
60
|
+
return modulePath.startsWith(rootDir) === false;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Find the layer level for a module
|
|
66
|
+
* @param {{[string]: string | {name: string, isNodeModule: boolean, interface: boolean}}} structure
|
|
67
|
+
* @returns the level of the module with `path`
|
|
68
|
+
*/
|
|
69
|
+
const findLevel = (structure) => {
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} path
|
|
72
|
+
*/
|
|
73
|
+
return (path) => {
|
|
74
|
+
const segments = toSegments(path);
|
|
75
|
+
const level = segments.reduce((level, _, index) => {
|
|
76
|
+
if (level >= 0) return level;
|
|
77
|
+
const path = toPath(segments.slice(0, segments.length - index));
|
|
78
|
+
return structure.findIndex((layer) => layer.name === path);
|
|
79
|
+
}, -1);
|
|
80
|
+
return level >= 0 ? level : null;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if there is an interface between file layer and module layer
|
|
86
|
+
* @param {{[string]: string | {name: string, isNodeModule: boolean, interface: boolean}}} structure
|
|
87
|
+
* @param {number} fileLevel
|
|
88
|
+
*/
|
|
89
|
+
const hasInterface = (structure, fileLevel) => {
|
|
90
|
+
/**
|
|
91
|
+
* @param {number} moduleLevel
|
|
92
|
+
*/
|
|
93
|
+
return (moduleLevel) => {
|
|
94
|
+
const layerInterface = structure
|
|
95
|
+
.slice(fileLevel + 1, moduleLevel)
|
|
96
|
+
.find((layer) => layer.interface);
|
|
97
|
+
return Boolean(layerInterface);
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
isNodeModule,
|
|
103
|
+
findLevel,
|
|
104
|
+
hasInterface,
|
|
105
|
+
createStructure,
|
|
106
|
+
createAliases,
|
|
107
|
+
removeAlias,
|
|
108
|
+
};
|