@gitlab/eslint-plugin 21.4.1 → 22.0.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/CHANGELOG.md +223 -0
- package/README.md +16 -11
- package/docs/development.md +2 -1
- package/docs/rules/vue-no-popover-target-dollar-el.md +69 -0
- package/docs/rules.md +1 -0
- package/docs/usage.md +158 -47
- package/eslint9/index.js +6 -7
- package/lib/configs/base/best-practices.js +0 -4
- package/lib/configs/base/es6.js +0 -12
- package/lib/configs/base/imports.js +0 -9
- package/lib/configs/base/node.js +0 -4
- package/lib/confusing-browser-globals.js +68 -0
- package/lib/flat-configs/base.js +122 -0
- package/lib/flat-configs/default.js +13 -0
- package/lib/flat-configs/i18n.js +14 -0
- package/lib/flat-configs/jest.js +20 -0
- package/lib/flat-configs/tailwind.js +19 -0
- package/{eslint9/configs → lib/flat-configs}/typescript.js +47 -62
- package/lib/flat-configs/vue.js +16 -0
- package/lib/index.js +30 -38
- package/lib/plugin.js +32 -0
- package/lib/prettier-rules.js +10 -0
- package/lib/rules/no-runtime-template-compiler.js +5 -2
- package/lib/rules/vue-no-new-non-primitive-in-template.js +1 -1
- package/lib/rules/vue-no-popover-target-dollar-el.js +156 -0
- package/lib/rules/vue-no-undef-apollo-properties.js +1 -1
- package/lib/rules/vue-prefer-dollar-scopedslots.js +1 -1
- package/lib/rules/vue-require-required-key.js +1 -1
- package/lib/rules/vue-slot-name-casing.js +2 -2
- package/lib/vue-fragments.js +111 -0
- package/package.json +29 -11
- package/scripts/generateRuleMapFixtures.js +75 -0
- package/scripts/helpers/getConfigs.js +13 -6
- package/scripts/integration_test/bootstrap.sh +1 -1
- package/scripts/updateFiles.js +30 -6
- package/eslint9/configs/base.js +0 -140
- package/lib/configs/base.js +0 -115
- package/lib/configs/default.js +0 -17
- package/lib/configs/i18n.js +0 -10
- package/lib/configs/jest.js +0 -21
- package/lib/configs/tailwind.js +0 -10
- package/lib/configs/typescript.js +0 -95
- package/lib/configs/vue.js +0 -90
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// ------------------------------------------------------------------------------
|
|
2
|
+
// Requirements
|
|
3
|
+
// ------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
const { DOCS_BASE_URL } = require('../constants');
|
|
6
|
+
const { defineTemplateBodyVisitor } = require('../utils/index');
|
|
7
|
+
|
|
8
|
+
// ------------------------------------------------------------------------------
|
|
9
|
+
// Constants
|
|
10
|
+
// ------------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Component names (both kebab-case and PascalCase) that use a `:target` prop
|
|
14
|
+
* and where passing a function returning `.$el` is unnecessary.
|
|
15
|
+
*/
|
|
16
|
+
const TARGET_COMPONENT_NAMES = new Set(['gl-popover', 'gl-tooltip', 'GlPopover', 'GlTooltip']);
|
|
17
|
+
|
|
18
|
+
const MESSAGE_ID = 'noPopoverTargetDollarEl';
|
|
19
|
+
|
|
20
|
+
// ------------------------------------------------------------------------------
|
|
21
|
+
// Helpers
|
|
22
|
+
// ------------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns true if the given VAttribute node is a `:target` or `v-bind:target`
|
|
26
|
+
* directive binding.
|
|
27
|
+
*
|
|
28
|
+
* @param {import('vue-eslint-parser').AST.VAttribute | import('vue-eslint-parser').AST.VDirective} node
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
function isTargetBinding(node) {
|
|
32
|
+
if (!node.directive) return false;
|
|
33
|
+
// Must be a v-bind directive
|
|
34
|
+
if (node.key.name.name !== 'bind') return false;
|
|
35
|
+
// Argument must be the identifier "target"
|
|
36
|
+
const arg = node.key.argument;
|
|
37
|
+
return arg != null && arg.type === 'VIdentifier' && arg.name === 'target';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns true if the given VElement node is a gl-popover, gl-tooltip,
|
|
42
|
+
* GlPopover, or GlTooltip component.
|
|
43
|
+
*
|
|
44
|
+
* @param {import('vue-eslint-parser').AST.VElement} element
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function isTargetComponent(element) {
|
|
48
|
+
return TARGET_COMPONENT_NAMES.has(element.rawName);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Given an expression node (the value of the `:target` binding), returns the
|
|
53
|
+
* MemberExpression node for the trailing `.$el` access if one exists, or null.
|
|
54
|
+
*
|
|
55
|
+
* Handles:
|
|
56
|
+
* - `() => $refs.foo.$el`
|
|
57
|
+
* - `() => this.$refs.foo.$el`
|
|
58
|
+
* - `() => { return $refs.foo.$el; }`
|
|
59
|
+
* - `function() { return $refs.foo.$el; }`
|
|
60
|
+
* - `function() { doSomething(); return this.$refs.foo.$el; }`
|
|
61
|
+
*
|
|
62
|
+
* @param {import('eslint').Rule.Node} expr
|
|
63
|
+
* @returns {import('eslint').Rule.Node | null}
|
|
64
|
+
*/
|
|
65
|
+
function getDollarElMemberExpression(expr) {
|
|
66
|
+
if (expr.type !== 'ArrowFunctionExpression' && expr.type !== 'FunctionExpression') {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let body = expr.body;
|
|
71
|
+
|
|
72
|
+
// This doesn't need to catch every case, just the most common ways of
|
|
73
|
+
// writing the target function, which is why this is simple/naive.
|
|
74
|
+
if (body.type === 'BlockStatement') {
|
|
75
|
+
const lastStatement = body.body.at(-1);
|
|
76
|
+
if (!lastStatement || lastStatement.type !== 'ReturnStatement') {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
body = lastStatement.argument;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!body) return null;
|
|
83
|
+
|
|
84
|
+
// The body (or return value) must be a MemberExpression ending in `.$el`
|
|
85
|
+
if (
|
|
86
|
+
body.type === 'MemberExpression' &&
|
|
87
|
+
!body.computed &&
|
|
88
|
+
body.property.type === 'Identifier' &&
|
|
89
|
+
body.property.name === '$el'
|
|
90
|
+
) {
|
|
91
|
+
return body;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ------------------------------------------------------------------------------
|
|
98
|
+
// Rule Definition
|
|
99
|
+
// ------------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
meta: {
|
|
103
|
+
type: 'suggestion',
|
|
104
|
+
docs: {
|
|
105
|
+
description:
|
|
106
|
+
'Disallow function-form `:target` bindings ending in `.$el` on `<gl-popover>` and `<gl-tooltip>`',
|
|
107
|
+
category: undefined,
|
|
108
|
+
url: DOCS_BASE_URL + '/vue-no-popover-target-dollar-el.md',
|
|
109
|
+
},
|
|
110
|
+
fixable: 'code',
|
|
111
|
+
messages: {
|
|
112
|
+
[MESSAGE_ID]:
|
|
113
|
+
'Return the component ref from the `:target` callback on {{component}} instead of its .$el.',
|
|
114
|
+
},
|
|
115
|
+
schema: [],
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
create(context) {
|
|
119
|
+
return defineTemplateBodyVisitor(context, {
|
|
120
|
+
VElement(element) {
|
|
121
|
+
// Must be a gl-popover or gl-tooltip component
|
|
122
|
+
if (!isTargetComponent(element)) return;
|
|
123
|
+
|
|
124
|
+
// Find the `:target` / `v-bind:target` binding among its attributes
|
|
125
|
+
const node = element.startTag.attributes.find(isTargetBinding);
|
|
126
|
+
if (!node) return;
|
|
127
|
+
|
|
128
|
+
// The binding value must be an expression container
|
|
129
|
+
const exprContainer = node.value;
|
|
130
|
+
if (!exprContainer || exprContainer.type !== 'VExpressionContainer') return;
|
|
131
|
+
|
|
132
|
+
const expr = exprContainer.expression;
|
|
133
|
+
if (!expr) return;
|
|
134
|
+
|
|
135
|
+
// Check whether the expression is a function/arrow whose body ends in `.$el`
|
|
136
|
+
const dollarElMember = getDollarElMemberExpression(expr);
|
|
137
|
+
if (!dollarElMember) return;
|
|
138
|
+
|
|
139
|
+
// `dollarElMember` is the full `$refs.foo.$el` MemberExpression.
|
|
140
|
+
// The object part (`$refs.foo`) is what we want to keep.
|
|
141
|
+
const objectNode = dollarElMember.object;
|
|
142
|
+
|
|
143
|
+
context.report({
|
|
144
|
+
node,
|
|
145
|
+
loc: node.loc,
|
|
146
|
+
messageId: MESSAGE_ID,
|
|
147
|
+
data: { component: element.rawName },
|
|
148
|
+
fix(fixer) {
|
|
149
|
+
// Remove the `.$el` suffix: from end of objectNode to end of dollarElMember
|
|
150
|
+
return fixer.removeRange([objectNode.range[1], dollarElMember.range[1]]);
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Requirements
|
|
3
3
|
// ------------------------------------------------------------------------------
|
|
4
4
|
const { DOCS_BASE_URL } = require('../constants');
|
|
5
|
-
const utils = require('eslint-plugin-vue/
|
|
5
|
+
const utils = require('eslint-plugin-vue/dist/utils').default;
|
|
6
6
|
|
|
7
7
|
// ------------------------------------------------------------------------------
|
|
8
8
|
// Helpers
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const { DOCS_BASE_URL } = require('../constants');
|
|
6
6
|
const { defineTemplateBodyVisitor } = require('../utils/index');
|
|
7
|
-
const utils = require('eslint-plugin-vue/
|
|
7
|
+
const utils = require('eslint-plugin-vue/dist/utils').default;
|
|
8
8
|
|
|
9
9
|
// ------------------------------------------------------------------------------
|
|
10
10
|
// Helpers
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// Requirements
|
|
5
5
|
// ------------------------------------------------------------------------------
|
|
6
6
|
|
|
7
|
-
const utils = require('eslint-plugin-vue/
|
|
8
|
-
const casing = require('eslint-plugin-vue/
|
|
7
|
+
const utils = require('eslint-plugin-vue/dist/utils').default;
|
|
8
|
+
const casing = require('eslint-plugin-vue/dist/utils/casing');
|
|
9
9
|
const { DOCS_BASE_URL } = require('../constants');
|
|
10
10
|
|
|
11
11
|
// ------------------------------------------------------------------------------
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const vuePlugin = require('eslint-plugin-vue');
|
|
2
|
+
const vueParser = require('vue-eslint-parser');
|
|
3
|
+
const globals = require('globals');
|
|
4
|
+
const unicornPlugin = require('eslint-plugin-unicorn');
|
|
5
|
+
const promisePlugin = require('eslint-plugin-promise');
|
|
6
|
+
const importPlugin = require('eslint-plugin-import');
|
|
7
|
+
|
|
8
|
+
const gitlabPlugin = require('./plugin.js');
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
The Vue config in two halves, so lib/flat-configs/vue.js and
|
|
12
|
+
lib/flat-configs/default.js can each place eslint-config-prettier where it
|
|
13
|
+
belongs for them: after the preset in the former, before it in the latter.
|
|
14
|
+
|
|
15
|
+
vue2-recommended, not the identically-named 'flat/recommended', which targets
|
|
16
|
+
Vue 3. The two names have never pointed at the same preset, and retargeting
|
|
17
|
+
would move 33 rules onto consumers -- a product decision, not a config-format
|
|
18
|
+
one.
|
|
19
|
+
*/
|
|
20
|
+
const presetAndSetup = [
|
|
21
|
+
...vuePlugin.configs['flat/vue2-recommended'],
|
|
22
|
+
{
|
|
23
|
+
name: '@gitlab/vue/setup',
|
|
24
|
+
languageOptions: {
|
|
25
|
+
parser: vueParser,
|
|
26
|
+
parserOptions: {
|
|
27
|
+
parser: require.resolve('espree'),
|
|
28
|
+
},
|
|
29
|
+
globals: {
|
|
30
|
+
...globals.browser,
|
|
31
|
+
...globals.es2020,
|
|
32
|
+
},
|
|
33
|
+
ecmaVersion: 'latest',
|
|
34
|
+
sourceType: 'module',
|
|
35
|
+
},
|
|
36
|
+
plugins: {
|
|
37
|
+
unicorn: unicornPlugin,
|
|
38
|
+
promise: promisePlugin,
|
|
39
|
+
import: importPlugin,
|
|
40
|
+
'@gitlab': gitlabPlugin,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const gitlabRules = [
|
|
46
|
+
{
|
|
47
|
+
name: '@gitlab/vue/rules',
|
|
48
|
+
rules: {
|
|
49
|
+
'vue/html-self-closing': [
|
|
50
|
+
'error',
|
|
51
|
+
{
|
|
52
|
+
html: { void: 'any', normal: 'never', component: 'always' },
|
|
53
|
+
svg: 'always',
|
|
54
|
+
math: 'always',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
'vue/block-order': ['error', { order: ['script', 'template', 'style'] }],
|
|
58
|
+
// Turning this off for now as we violate this rule in many places.
|
|
59
|
+
'vue/max-attributes-per-line': 'off',
|
|
60
|
+
'vue/component-options-name-casing': ['error', 'PascalCase'],
|
|
61
|
+
'vue/component-name-in-template-casing': ['error', 'kebab-case'],
|
|
62
|
+
'@gitlab/vue-require-required-key': 'error',
|
|
63
|
+
'vue/v-slot-style': ['error', { atComponent: 'shorthand' }],
|
|
64
|
+
'@gitlab/vue-no-data-toggle': 'error',
|
|
65
|
+
'@gitlab/vue-no-popover-target-dollar-el': 'warn',
|
|
66
|
+
'@gitlab/vue-prefer-dollar-scopedslots': 'error',
|
|
67
|
+
'@gitlab/vue-slot-name-casing': 'error',
|
|
68
|
+
'@gitlab/no-runtime-template-compiler': 'error',
|
|
69
|
+
'import/order': [
|
|
70
|
+
'error',
|
|
71
|
+
{
|
|
72
|
+
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
// See https://gitlab.com/gitlab-org/frontend/eslint-plugin/-/issues/52.
|
|
76
|
+
'vue/component-api-style': ['error', ['options']],
|
|
77
|
+
// We might not want to enable vue/singleline-html-element-content-newline and
|
|
78
|
+
// vue/multiline-html-element-content-newline just yet as they might cause
|
|
79
|
+
// regression depending on how the consumers compile Vue templates.
|
|
80
|
+
'vue/singleline-html-element-content-newline': 'off',
|
|
81
|
+
'vue/multiline-html-element-content-newline': 'off',
|
|
82
|
+
// Disabling these as they might conflict with prettier.
|
|
83
|
+
'vue/html-indent': 'off',
|
|
84
|
+
'vue/html-closing-bracket-newline': 'off',
|
|
85
|
+
// BEGIN rules to aid migration from Vue 2.x to 3.x.
|
|
86
|
+
// See https://gitlab.com/groups/gitlab-org/-/epics/3174 for more details.
|
|
87
|
+
'vue/no-deprecated-data-object-declaration': 'error',
|
|
88
|
+
'vue/no-deprecated-events-api': 'error',
|
|
89
|
+
'vue/no-deprecated-filter': 'error',
|
|
90
|
+
'vue/no-deprecated-functional-template': 'error',
|
|
91
|
+
'vue/no-deprecated-html-element-is': 'error',
|
|
92
|
+
'vue/no-deprecated-inline-template': 'error',
|
|
93
|
+
'vue/no-deprecated-props-default-this': 'error',
|
|
94
|
+
'vue/no-deprecated-scope-attribute': 'error',
|
|
95
|
+
'vue/no-deprecated-slot-attribute': 'error',
|
|
96
|
+
'vue/no-deprecated-slot-scope-attribute': 'error',
|
|
97
|
+
'vue/no-deprecated-v-on-number-modifiers': 'error',
|
|
98
|
+
'vue/no-deprecated-vue-config-keycodes': 'error',
|
|
99
|
+
// END rules to aid migration from Vue 2.x to 3.x.
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: '@gitlab/vue/sfc-overrides',
|
|
104
|
+
files: ['**/*.vue'],
|
|
105
|
+
rules: {
|
|
106
|
+
'import/no-default-export': 'off',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
module.exports = { presetAndSetup, gitlabRules };
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitlab/eslint-plugin",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "22.0.0",
|
|
4
4
|
"description": "GitLab package for our custom eslint rules",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"createRule": "./scripts/createRule.js && yarn update",
|
|
8
|
-
"update": "./scripts/updateFiles.js && prettier --write lib/index.js",
|
|
8
|
+
"update": "./scripts/updateFiles.js && prettier --write lib/plugin.js lib/index.js",
|
|
9
9
|
"commit": "npx git-cz",
|
|
10
10
|
"test": "mocha"
|
|
11
11
|
},
|
|
@@ -27,30 +27,48 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://gitlab.com/gitlab-org/frontend/eslint-plugin#readme",
|
|
29
29
|
"engines": {
|
|
30
|
-
"node": ">=
|
|
30
|
+
"node": "^20.12.0 || ^22.0.0 || >=24.0.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@
|
|
34
|
-
"
|
|
35
|
-
"eslint-config-prettier": "^9.1.0",
|
|
33
|
+
"@eslint/js": "^9.0.0",
|
|
34
|
+
"eslint-config-prettier": "^10.0.0",
|
|
36
35
|
"eslint-plugin-import": "^2.29.1",
|
|
37
|
-
"eslint-plugin-jest": "^
|
|
36
|
+
"eslint-plugin-jest": "^29.0.0",
|
|
38
37
|
"eslint-plugin-promise": "^7.0.0",
|
|
39
38
|
"eslint-plugin-tailwindcss": "^3.18.3",
|
|
40
39
|
"eslint-plugin-unicorn": "^55.0.0",
|
|
41
|
-
"eslint-plugin-vue": "^
|
|
40
|
+
"eslint-plugin-vue": "^10.0.0",
|
|
41
|
+
"globals": "^17.0.0",
|
|
42
42
|
"lodash": "^4.18.1",
|
|
43
|
-
"
|
|
43
|
+
"semver": "^7.0.0",
|
|
44
|
+
"vue-eslint-parser": "^10.3.0"
|
|
44
45
|
},
|
|
45
46
|
"peerDependencies": {
|
|
46
|
-
"eslint": "^8.
|
|
47
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
48
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
49
|
+
"eslint": "^9.0.0",
|
|
50
|
+
"typescript": ">=4.8.4"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"@typescript-eslint/eslint-plugin": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"@typescript-eslint/parser": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
59
|
+
"typescript": {
|
|
60
|
+
"optional": true
|
|
61
|
+
}
|
|
47
62
|
},
|
|
48
63
|
"devDependencies": {
|
|
49
64
|
"@changesets/cli": "^2.29.8",
|
|
50
|
-
"eslint": "^8.
|
|
65
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
66
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
67
|
+
"eslint": "^9.0.0",
|
|
51
68
|
"glob": "^7.2.0",
|
|
52
69
|
"mocha": "^11.7.5",
|
|
53
70
|
"prettier": "^2.6.1",
|
|
71
|
+
"typescript": "^5.0.0",
|
|
54
72
|
"yarn-deduplicate": "^6.0.2"
|
|
55
73
|
}
|
|
56
74
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ONE-SHOT generator. Resolves each eslintrc config in lib/configs/ into a
|
|
4
|
+
* flat map of rule -> [severity, ...options] and writes it to
|
|
5
|
+
* tests/fixtures/rule-maps/. Run once, on ESLint 8, BEFORE the flat-config
|
|
6
|
+
* migration. Committed for reviewability; not wired into CI.
|
|
7
|
+
*
|
|
8
|
+
* Its inputs are gone and it no longer runs. Kept only as the provenance of
|
|
9
|
+
* the frozen fixtures; deleted at the end of this stack.
|
|
10
|
+
*/
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { ESLint } = require('eslint');
|
|
14
|
+
|
|
15
|
+
const CONFIG_DIR = path.join(__dirname, '../lib/configs');
|
|
16
|
+
const OUT_DIR = path.join(__dirname, '../tests/fixtures/rule-maps');
|
|
17
|
+
|
|
18
|
+
// Top level only: lib/configs/base/ holds rule fragments, not configs.
|
|
19
|
+
const CONFIGS = fs
|
|
20
|
+
.readdirSync(CONFIG_DIR)
|
|
21
|
+
.filter((file) => file.endsWith('.js'))
|
|
22
|
+
.map((file) => path.basename(file, '.js'));
|
|
23
|
+
const SAMPLES = ['sample.js', 'sample.vue', 'sample.ts', 'sample.test.ts'];
|
|
24
|
+
|
|
25
|
+
const SEVERITIES = ['off', 'warn', 'error'];
|
|
26
|
+
|
|
27
|
+
/** Normalise [2, opts] / 'error' / ['error', opts] into 'error' or ['error', ...opts]. */
|
|
28
|
+
function normalizeEntry(entry) {
|
|
29
|
+
const value = Array.isArray(entry) ? entry : [entry];
|
|
30
|
+
const [severity, ...options] = value;
|
|
31
|
+
const name = typeof severity === 'number' ? SEVERITIES[severity] : severity;
|
|
32
|
+
return options.length > 0 ? [name, ...options] : name;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeRules(rules) {
|
|
36
|
+
return Object.fromEntries(
|
|
37
|
+
Object.keys(rules)
|
|
38
|
+
.sort()
|
|
39
|
+
.map((name) => [name, normalizeEntry(rules[name])]),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function mapForConfig(configName) {
|
|
44
|
+
const configPath = require.resolve(`../lib/configs/${configName}.js`);
|
|
45
|
+
const eslint = new ESLint({
|
|
46
|
+
useEslintrc: false,
|
|
47
|
+
// The plugin cannot resolve itself by name from inside its own repo.
|
|
48
|
+
plugins: { '@gitlab': require('../lib/index.js') },
|
|
49
|
+
baseConfig: { extends: [configPath] },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const resolved = await Promise.all(
|
|
53
|
+
SAMPLES.map((sample) => eslint.calculateConfigForFile(path.join(process.cwd(), sample))),
|
|
54
|
+
);
|
|
55
|
+
return Object.fromEntries(
|
|
56
|
+
SAMPLES.map((sample, index) => [sample, normalizeRules(resolved[index].rules || {})]),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
62
|
+
for (const name of CONFIGS) {
|
|
63
|
+
// eslint-disable-next-line no-await-in-loop
|
|
64
|
+
const map = await mapForConfig(name);
|
|
65
|
+
const file = path.join(OUT_DIR, `${name}.json`);
|
|
66
|
+
fs.writeFileSync(file, `${JSON.stringify(map, null, 2)}\n`);
|
|
67
|
+
const counts = SAMPLES.map((sample) => `${sample} ${Object.keys(map[sample]).length}`);
|
|
68
|
+
console.log(`Wrote ${path.relative(process.cwd(), file)} (${counts.join(', ')})`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
main().catch((error) => {
|
|
73
|
+
console.error(error);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
});
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const glob = require('glob');
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const configsDir = path.join(__dirname, '../../lib/flat-configs');
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/*
|
|
7
|
+
lib/flat-configs/ is a closed set: this glob decides what `yarn update`
|
|
8
|
+
publishes as `plugin.configs[<basename>]`, so a shared helper dropped in that
|
|
9
|
+
directory becomes a bogus public config. That is why lib/prettier-rules.js and
|
|
10
|
+
lib/vue-fragments.js sit at the lib/ root instead; put new shared code there.
|
|
11
|
+
|
|
12
|
+
No `.meta` spread, unlike getRules.js: a flat config exports an array.
|
|
13
|
+
*/
|
|
14
|
+
function getConfig(fullPath) {
|
|
15
|
+
const relative = path.relative(configsDir, fullPath);
|
|
8
16
|
const name = path.basename(relative, '.js');
|
|
9
17
|
return {
|
|
10
|
-
...require(fullPath).meta,
|
|
11
18
|
name,
|
|
12
19
|
relative,
|
|
13
20
|
fullPath,
|
|
14
21
|
};
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
module.exports = function
|
|
18
|
-
return glob.sync(path.join(
|
|
24
|
+
module.exports = function getConfigs() {
|
|
25
|
+
return glob.sync(path.join(configsDir, '*.js')).map(getConfig);
|
|
19
26
|
};
|
|
@@ -35,7 +35,7 @@ else
|
|
|
35
35
|
fi
|
|
36
36
|
|
|
37
37
|
echo "Installing $CI_REPOSITORY_URL#$SHA"
|
|
38
|
-
yarn add --dev "${CI_REPOSITORY_URL}#${SHA}"
|
|
38
|
+
yarn add --dev --ignore-workspace-root-check "${CI_REPOSITORY_URL}#${SHA}"
|
|
39
39
|
|
|
40
40
|
echo "Deduplicate dependencies"
|
|
41
41
|
# Yarn doesn't seem to be smart enough when using URL installs and might
|
package/scripts/updateFiles.js
CHANGED
|
@@ -9,21 +9,40 @@ const libPath = path.join(__dirname, '../lib');
|
|
|
9
9
|
const rules = getRules();
|
|
10
10
|
const configs = getConfigs();
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const createPluginJs = (ruleImports) => `/**
|
|
13
13
|
* This file is GENERATED, please run \`yarn update\` after adding or renaming a rule
|
|
14
14
|
*/
|
|
15
|
+
const { name, version } = require('../package.json');
|
|
16
|
+
|
|
15
17
|
module.exports = {
|
|
18
|
+
meta: { name, version },
|
|
16
19
|
rules: {
|
|
17
20
|
${ruleImports.join('\n')}
|
|
18
21
|
},
|
|
22
|
+
};
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
const createIndexJs = (configImports) => `/**
|
|
26
|
+
* This file is GENERATED, please run \`yarn update\` after adding or renaming a rule
|
|
27
|
+
*/
|
|
28
|
+
const plugin = require('./plugin.js');
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
Getters, so requiring the plugin never loads a config the consumer is not
|
|
32
|
+
using: \`typescript\` pulls in optional peer dependencies, and eagerly
|
|
33
|
+
requiring it would throw for anyone without the TypeScript toolchain. Getters
|
|
34
|
+
in an object literal are enumerable, so \`Object.keys(configs)\` is unaffected.
|
|
35
|
+
*/
|
|
36
|
+
module.exports = {
|
|
37
|
+
...plugin,
|
|
19
38
|
configs: {
|
|
20
39
|
${configImports.join('\n')}
|
|
21
|
-
}
|
|
40
|
+
},
|
|
22
41
|
};
|
|
23
42
|
`;
|
|
24
43
|
|
|
25
44
|
const writeIndexJs = () => {
|
|
26
|
-
console.log('Updating lib/index.js...');
|
|
45
|
+
console.log('Updating lib/plugin.js and lib/index.js...');
|
|
27
46
|
|
|
28
47
|
const ruleImports = rules.map((rule) => {
|
|
29
48
|
const relPath = path.relative(libPath, rule.fullPath);
|
|
@@ -32,12 +51,17 @@ const writeIndexJs = () => {
|
|
|
32
51
|
|
|
33
52
|
const configImports = configs.map((config) => {
|
|
34
53
|
const relPath = path.relative(libPath, config.fullPath);
|
|
35
|
-
|
|
54
|
+
// Quoted so a hyphenated filename cannot emit a syntax error; prettier
|
|
55
|
+
// drops the quotes again where they are redundant.
|
|
56
|
+
return ` get '${config.name}'() {
|
|
57
|
+
return require('./${relPath}');
|
|
58
|
+
},`;
|
|
36
59
|
});
|
|
37
60
|
|
|
38
|
-
fs.writeFileSync(path.join(libPath, '
|
|
61
|
+
fs.writeFileSync(path.join(libPath, 'plugin.js'), createPluginJs(ruleImports));
|
|
62
|
+
fs.writeFileSync(path.join(libPath, 'index.js'), createIndexJs(configImports));
|
|
39
63
|
|
|
40
|
-
console.log('Successfully updated lib/index.js');
|
|
64
|
+
console.log('Successfully updated lib/plugin.js and lib/index.js');
|
|
41
65
|
};
|
|
42
66
|
|
|
43
67
|
const createDoc = (ruleDocs) => `<!--
|