linter-bundle 3.10.0 → 4.0.1
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/.linter-bundle.js +51 -0
- package/CHANGELOG.md +26 -1
- package/README.md +372 -93
- package/TODO.md +10 -19
- package/eslint/index.js +6 -5
- package/eslint/overrides-gatsby.js +2 -2
- package/eslint/overrides-javascript.js +1 -1
- package/eslint/overrides-react.js +3 -2
- package/eslint/overrides-type-declarations.js +1 -1
- package/eslint/rules/no-global-undefined-check.md +1 -1
- package/eslint/rules/restricted-filenames.js +6 -2
- package/eslint/rules/restricted-filenames.md +20 -16
- package/files/index.js +100 -0
- package/helper/config.js +48 -0
- package/helper/get-git-files.js +52 -0
- package/helper/run-process.js +2 -1
- package/lint.js +330 -196
- package/package.json +3 -3
- package/stylelint/index.js +4 -2
|
@@ -92,7 +92,7 @@ module.exports = {
|
|
|
92
92
|
*
|
|
93
93
|
* @see https://github.com/sindresorhus/eslint-plugin-unicorn
|
|
94
94
|
*/
|
|
95
|
-
'unicorn/filename-case': ['
|
|
95
|
+
'unicorn/filename-case': ['off', { // Disabled in favour of the linter-bundle `files` task
|
|
96
96
|
cases: {
|
|
97
97
|
kebabCase: true
|
|
98
98
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @file Settings for React code in TypeScript (TSX) files.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
const config = require('../helper/config.js');
|
|
5
6
|
const ensureType = require('../helper/ensure-type');
|
|
6
7
|
|
|
7
8
|
module.exports = {
|
|
@@ -90,11 +91,11 @@ module.exports = {
|
|
|
90
91
|
'react/forbid-component-props': ['error', { forbid: [
|
|
91
92
|
{
|
|
92
93
|
propName: 'className',
|
|
93
|
-
allowedFor: ensureType.array(
|
|
94
|
+
allowedFor: ensureType.array(config.ts?.overrides?.react?.['react/forbid-component-props']?.allowClassNameFor)
|
|
94
95
|
},
|
|
95
96
|
{
|
|
96
97
|
propName: 'style',
|
|
97
|
-
allowedFor: ensureType.array(
|
|
98
|
+
allowedFor: ensureType.array(config.ts?.overrides?.react?.['react/forbid-component-props']?.allowStyleFor)
|
|
98
99
|
}
|
|
99
100
|
] }],
|
|
100
101
|
'react/forbid-dom-props': 'error',
|
|
@@ -28,7 +28,7 @@ module.exports = {
|
|
|
28
28
|
*
|
|
29
29
|
* @see https://github.com/sindresorhus/eslint-plugin-unicorn
|
|
30
30
|
*/
|
|
31
|
-
'unicorn/filename-case': ['
|
|
31
|
+
'unicorn/filename-case': ['off', { // Disabled in favour of the linter-bundle `files` task
|
|
32
32
|
cases: {
|
|
33
33
|
kebabCase: true,
|
|
34
34
|
pascalCase: true
|
|
@@ -6,7 +6,7 @@ Rendering different output, depending on whether it is SSR or CSR, can lead to h
|
|
|
6
6
|
|
|
7
7
|
Examples of **incorrect** code for this rule:
|
|
8
8
|
|
|
9
|
-
```
|
|
9
|
+
```js
|
|
10
10
|
if ('undefined' === typeof window) {}
|
|
11
11
|
if ('undefined' == typeof window) {}
|
|
12
12
|
if (typeof window !== 'undefined') {}
|
|
@@ -6,6 +6,8 @@ const path = require('node:path');
|
|
|
6
6
|
|
|
7
7
|
const micromatch = require('micromatch');
|
|
8
8
|
|
|
9
|
+
const config = require('../../helper/config.js');
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* @type {import('eslint').Rule.RuleModule}
|
|
11
13
|
*/
|
|
@@ -41,7 +43,8 @@ module.exports = {
|
|
|
41
43
|
type: 'string'
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
|
-
}
|
|
46
|
+
},
|
|
47
|
+
required: ['basePath']
|
|
45
48
|
}
|
|
46
49
|
]
|
|
47
50
|
}
|
|
@@ -49,7 +52,8 @@ module.exports = {
|
|
|
49
52
|
create: (context) => {
|
|
50
53
|
const filePath = context.getFilename();
|
|
51
54
|
/** @type {{ basePath: string, allowed?: string[]; disallowed?: string[]; }[]} */
|
|
52
|
-
|
|
55
|
+
// eslint-disable-next-line n/no-process-env -- Only merge the linter-bundle config, if the linting is not started by the linter-bundle CLI tool (e.g. if ESlint is running separately in VSCode), to get warnings shown there too
|
|
56
|
+
const options = (!process.env['LINTER_BUNDLE'] && config.files?.restrictions ? [...config.files.restrictions, ...context.options] : context.options);
|
|
53
57
|
|
|
54
58
|
for (const { basePath, allowed, disallowed } of options) {
|
|
55
59
|
const normalizedName = path.relative(path.join(process.cwd(), basePath), filePath);
|
|
@@ -6,7 +6,7 @@ In projects in which several developers or even teams work together, it is impor
|
|
|
6
6
|
|
|
7
7
|
Example configuration for this rule:
|
|
8
8
|
|
|
9
|
-
```
|
|
9
|
+
```js
|
|
10
10
|
'restricted-filenames': ['error', {
|
|
11
11
|
basePath: './src',
|
|
12
12
|
allowed: [
|
|
@@ -48,15 +48,17 @@ If only `disallowed` is set, all unspecified files are allowed.
|
|
|
48
48
|
|
|
49
49
|
If both are set, `disallowed` wins over `allowed` and all unspecified files are disallowed.
|
|
50
50
|
|
|
51
|
-
##
|
|
51
|
+
## Useful reusable code snippets in Glob patterns
|
|
52
52
|
|
|
53
53
|
| Name | Example | Glob pattern |
|
|
54
54
|
|-|-|-|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
| Lower case | DONUTS | `[a-z]*([a-z0-9])` |
|
|
56
|
+
| Upper case | donuts | `[A-Z]*([A-Z0-9])` |
|
|
57
|
+
| Snake case | number_of_donuts | `[a-z]*(*([a-z0-9]-)+([a-z0-9]))` |
|
|
58
|
+
| Screaming snake case | NUMBER_OF_DONUTS | `[A-Z]*(*([A-Z0-9]_)+([A-Z0-9]))` |
|
|
59
|
+
| Kebab case | number-of-donuts | `[a-z]*(*([a-z0-9]-)+([a-z0-9]))` |
|
|
60
|
+
| Camel case | numberOfDonuts | `[a-z]*([a-zA-Z0-9])` |
|
|
61
|
+
| Pascal case | NumberOfDonuts | `[A-Z]*([a-zA-Z0-9])` |
|
|
60
62
|
|
|
61
63
|
## Glob pattern templates
|
|
62
64
|
|
|
@@ -64,13 +66,15 @@ Instead of defining the same complex patterns over and over again, e.g. for casi
|
|
|
64
66
|
|
|
65
67
|
Example of `.eslintrc.js`:
|
|
66
68
|
|
|
67
|
-
```
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
```js
|
|
70
|
+
const snippets = {
|
|
71
|
+
lowerCase: '[a-z]*([a-z0-9])',
|
|
72
|
+
upperCase: '[A-Z]*([A-Z0-9])',
|
|
73
|
+
snakeCase: '[a-z]*(*([a-z0-9]_)+([a-z0-9]))',
|
|
74
|
+
screamingSnakeCase: '[A-Z]*(*([A-Z0-9]_)+([A-Z0-9]))',
|
|
75
|
+
kebabCase: '[a-z]*(*([a-z0-9]-)+([a-z0-9]))',
|
|
76
|
+
camelCase: '[a-z]*([a-zA-Z0-9])',
|
|
77
|
+
pascalCase: '[A-Z]*([a-zA-Z0-9])'
|
|
74
78
|
};
|
|
75
79
|
|
|
76
80
|
module.exports = {
|
|
@@ -78,8 +82,8 @@ module.exports = {
|
|
|
78
82
|
'restricted-filenames': ['error', {
|
|
79
83
|
basePath: './src',
|
|
80
84
|
allowed: [
|
|
81
|
-
`components/${
|
|
82
|
-
`utils/{index.ts,lib/${
|
|
85
|
+
`components/${snippets.pascalCase}/index.tsx`,
|
|
86
|
+
`utils/{index.ts,lib/${snippets.camelCase}?(.spec).ts}`
|
|
83
87
|
]
|
|
84
88
|
}]
|
|
85
89
|
}
|
package/files/index.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Ensures that only files which match given glob patterns are part of the project.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
|
|
8
|
+
const micromatch = require('micromatch');
|
|
9
|
+
|
|
10
|
+
const config = require('../helper/config.js');
|
|
11
|
+
|
|
12
|
+
const restrictions = config.files?.restrictions;
|
|
13
|
+
|
|
14
|
+
if (Array.isArray(restrictions)) {
|
|
15
|
+
let noOfErrors = 0;
|
|
16
|
+
|
|
17
|
+
const argFiles = process.argv.splice(2).map((filePath) => path.join(process.cwd(), filePath));
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reports the error to the `console` and increase the error counter.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} filePath - Path to the file which is not allowed
|
|
23
|
+
* @returns {void}
|
|
24
|
+
*/
|
|
25
|
+
const report = (filePath) => {
|
|
26
|
+
process.stderr.write(`Disallowed filename "${filePath}"\n`);
|
|
27
|
+
|
|
28
|
+
noOfErrors++;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
for (const { basePath, allowed, disallowed } of restrictions) {
|
|
32
|
+
const absoluteBasePath = path.join(process.cwd(), basePath);
|
|
33
|
+
|
|
34
|
+
const files = (argFiles.length > 0 ? argFiles.filter((filePath) => !path.relative(absoluteBasePath, filePath).startsWith('..')) : getFiles(absoluteBasePath));
|
|
35
|
+
|
|
36
|
+
for (const filePath of files) {
|
|
37
|
+
const normalizedFilePath = path.relative(absoluteBasePath, filePath);
|
|
38
|
+
|
|
39
|
+
if (allowed && !disallowed) {
|
|
40
|
+
if (!micromatch.isMatch(normalizedFilePath, allowed, { dot: true })) {
|
|
41
|
+
report(normalizedFilePath);
|
|
42
|
+
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (!allowed && disallowed) {
|
|
47
|
+
if (micromatch.isMatch(normalizedFilePath, disallowed, { dot: true })) {
|
|
48
|
+
report(normalizedFilePath);
|
|
49
|
+
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (allowed && disallowed) {
|
|
54
|
+
if (
|
|
55
|
+
micromatch.isMatch(normalizedFilePath, disallowed, { dot: true }) ||
|
|
56
|
+
!micromatch.isMatch(normalizedFilePath, allowed, { dot: true })
|
|
57
|
+
) {
|
|
58
|
+
report(normalizedFilePath);
|
|
59
|
+
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (noOfErrors > 0) {
|
|
67
|
+
process.stderr.write(`\nFound ${noOfErrors} disallowed file${(noOfErrors !== 0 ? 's' : '')}\n`);
|
|
68
|
+
|
|
69
|
+
process.exitCode = -1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
process.stderr.write('No file restrictions found\n');
|
|
74
|
+
|
|
75
|
+
process.exitCode = -1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns a list of all files within a given directory.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} directory - Base directory for the file search
|
|
82
|
+
* @param {string[]} [files] - List of files which are already found (is this parameter unset for the initial call)
|
|
83
|
+
* @returns {string[]} List of files which have been found
|
|
84
|
+
*/
|
|
85
|
+
function getFiles (directory, files = []) {
|
|
86
|
+
const fileList = fs.readdirSync(directory);
|
|
87
|
+
|
|
88
|
+
for (const file of fileList) {
|
|
89
|
+
const name = `${directory}/${file}`;
|
|
90
|
+
|
|
91
|
+
if (fs.statSync(name).isDirectory()) {
|
|
92
|
+
getFiles(name, files);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
files.push(name);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return files;
|
|
100
|
+
}
|
package/helper/config.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Returns the `.linter-bundle.js` configuration result.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef linterBundleConfig
|
|
10
|
+
* @property {boolean} [verbose]
|
|
11
|
+
* @property {boolean} [timing]
|
|
12
|
+
* @property {boolean} [git]
|
|
13
|
+
* @property {{ tsconfig?: string; }} [tsc]
|
|
14
|
+
* @property {{ tsconfig?: string; include?: string[]; exclude?: string[]; overrides?: {
|
|
15
|
+
* general?: {
|
|
16
|
+
* 'no-restricted-globals'?: { additionalRestrictions?: { name: string; message: string; }[]; },
|
|
17
|
+
* 'no-restricted-properties'?: { additionalRestrictions?: { object: string; property: string; message: string; }[]; },
|
|
18
|
+
* 'no-restricted-syntax'?: { additionalRestrictions?: { selector: string; message: string; }[]; },
|
|
19
|
+
* 'import/order'?: { additionalExternalPatterns?: string[]; }
|
|
20
|
+
* },
|
|
21
|
+
* react?: {
|
|
22
|
+
* 'react/forbid-component-props'?: { allowClassNameFor?: string[]; allowStyleFor?: string[]; }
|
|
23
|
+
* }
|
|
24
|
+
* }; }} [ts]
|
|
25
|
+
* @property {{ patternPrefix?: string; }} [sass]
|
|
26
|
+
* @property {{ minSeverity?: 'info' | 'low' | 'moderate' | 'high' | 'critical'; exclude?: string[]; }} [audit]
|
|
27
|
+
* @property {{ restrictions: { basePath: string; allowed?: string[]; disallowed?: string[]; }[]; }} [files]
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
module.exports = (
|
|
31
|
+
loadConfig('.linter-bundle.json') ??
|
|
32
|
+
loadConfig('.linter-bundle.cjs') ??
|
|
33
|
+
loadConfig('.linter-bundle.js') ??
|
|
34
|
+
{}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load a config file if it exist.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} fileName - The name of the config file
|
|
41
|
+
* @returns {linterBundleConfig | undefined} - Either the file content for `undefined` if the file does not exist.
|
|
42
|
+
* */
|
|
43
|
+
function loadConfig (fileName) {
|
|
44
|
+
const filePath = path.join(process.cwd(), fileName);
|
|
45
|
+
|
|
46
|
+
// eslint-disable-next-line import/no-dynamic-require -- Required here to load the configuration file.
|
|
47
|
+
return (fs.existsSync(filePath) ? require(filePath) : undefined);
|
|
48
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Handling of changed files from Git.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { runProcess } = require('./run-process.js');
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('./run-process').ProcessResult} ProcessResult */
|
|
8
|
+
|
|
9
|
+
/** @type {{ diff: Promise<ProcessResult>; modified: Promise<ProcessResult>; deleted: Promise<ProcessResult>; } | undefined} */
|
|
10
|
+
let gitFilesProcessPromise;
|
|
11
|
+
/** @type {string[] | undefined} */
|
|
12
|
+
let gitFiles;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns a list of changed files based on Git.
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
* @returns {Promise<string[]>} The list of changed files.
|
|
19
|
+
*/
|
|
20
|
+
async function getGitFiles () {
|
|
21
|
+
if (!gitFilesProcessPromise) {
|
|
22
|
+
gitFilesProcessPromise = {
|
|
23
|
+
// Returns changed files, also stashed and committed
|
|
24
|
+
diff: runProcess('git diff --name-only -z @{upstream}'),
|
|
25
|
+
// Returns unstashed files (including deleted)
|
|
26
|
+
modified: runProcess('git ls-files -o -m --exclude-standard --full-name --deduplicate -z'),
|
|
27
|
+
// Returns unstashed, deleted files - @todo Is there a way to also get a list of deleted stashed/committed files?
|
|
28
|
+
deleted: runProcess('git ls-files -d --exclude-standard --full-name --deduplicate -z')
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const gitProcessResult = {
|
|
33
|
+
diff: await gitFilesProcessPromise.diff,
|
|
34
|
+
modified: await gitFilesProcessPromise.modified,
|
|
35
|
+
deleted: await gitFilesProcessPromise.deleted
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (!gitFiles) {
|
|
39
|
+
const deletedFiles = gitProcessResult.deleted.stdout.trim().split('\0');
|
|
40
|
+
|
|
41
|
+
gitFiles = [
|
|
42
|
+
...gitProcessResult.diff.stdout.trim().split('\0'),
|
|
43
|
+
...gitProcessResult.modified.stdout.trim().split('\0')
|
|
44
|
+
].filter((file, index, self) => !deletedFiles.includes(file) && self.indexOf(file) === index);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return gitFiles;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
getGitFiles
|
|
52
|
+
};
|
package/helper/run-process.js
CHANGED
|
@@ -25,7 +25,8 @@ async function runProcess (command, options) {
|
|
|
25
25
|
/** @type {string[]} */
|
|
26
26
|
const stderr = [];
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
// eslint-disable-next-line n/no-process-env -- We need to access `process.env`, because this is the default value if `env` is not set.
|
|
29
|
+
const lintingProcess = childProcess.exec(command, { ...options, env: { ...process.env, ...options?.env, LINTER_BUNDLE: '1' }, shell: os.userInfo().shell });
|
|
29
30
|
|
|
30
31
|
lintingProcess.stdout?.on('data', (/** @type {string} */data) => {
|
|
31
32
|
stdout.push(data);
|