@gitlab/eslint-plugin 20.0.0 → 20.2.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 CHANGED
@@ -1,3 +1,17 @@
1
+ # [20.2.0](https://gitlab.com/gitlab-org/frontend/eslint-plugin/compare/v20.1.0...v20.2.0) (2024-09-03)
2
+
3
+
4
+ ### Features
5
+
6
+ * add rule for uninitialised apollo query properties ([f89eace](https://gitlab.com/gitlab-org/frontend/eslint-plugin/commit/f89eace7498900d90e06b0be214c51a589474d53)), closes [#74](https://gitlab.com/gitlab-org/frontend/eslint-plugin/issues/74)
7
+
8
+ # [20.1.0](https://gitlab.com/gitlab-org/frontend/eslint-plugin/compare/v20.0.0...v20.1.0) (2024-08-28)
9
+
10
+
11
+ ### Features
12
+
13
+ * add rules to ensure Tailwind CSS is used properly ([c51533e](https://gitlab.com/gitlab-org/frontend/eslint-plugin/commit/c51533ecfa9f20f76d0b2a3113da7213213658f6))
14
+
1
15
  # [20.0.0](https://gitlab.com/gitlab-org/frontend/eslint-plugin/compare/v19.6.1...v20.0.0) (2024-08-13)
2
16
 
3
17
 
@@ -0,0 +1,38 @@
1
+ # @gitlab/tailwind
2
+
3
+ This rule lints against string interpolation being used to build CSS utility class
4
+ names as that would prevent Tailwind from parsing those classes and adding them to the bundle.
5
+
6
+ ## Rule Details
7
+
8
+ ### Examples of **incorrect** code for this rule
9
+
10
+ ```js
11
+ // Using interpolation to build a utility in a string literal
12
+ const cssUtilsStringLiteral = 'gl-bg-red-' + variant;
13
+
14
+ // Using interpolation to build a utility in a template literal
15
+ const cssUtilsTemplateLiteral = `gl-border-top gl-bg-red-${color} gl-text-gray-400`;
16
+ ```
17
+
18
+ ### Examples of **correct** code for this rule
19
+
20
+ ```js
21
+ // Utilities declared as a string literal without interpolation
22
+ const cssUtilsStringLiteral = 'gl-bg-red-800';
23
+
24
+ // Utilities declared as a template literal without interpolation
25
+ const cssUtils = `gl-border-top gl-bg-red-800 gl-text-gray-400`;
26
+ ```
27
+
28
+ ## Options
29
+
30
+ No options.
31
+
32
+ ## Related rules
33
+
34
+ - [vue-tailwind](./vue-tailwind.md)
35
+
36
+ ## When Not To Use It
37
+
38
+ If the codebase doesn't leverage Tailwind CSS, keep this rule disabled.
@@ -0,0 +1,95 @@
1
+ # @gitlab/vue-no-undef-apollo-properties
2
+
3
+ Require Apollo query properties to be initialized in component data.
4
+
5
+ Sensible initial values for Apollo queries should be defined in component data.
6
+ This reduces the likelihood of unexpected behaviour, especially when running
7
+ under @vue/compat.
8
+
9
+ This rule does not detect whether the initial value is of the correct type.
10
+
11
+ ## Rule Details
12
+
13
+ ### Examples of **incorrect** code for this rule
14
+
15
+ ```html
16
+ <script>
17
+ export default {
18
+ data() {
19
+ return {
20
+ foo: '',
21
+ qux: undefined,
22
+ };
23
+ },
24
+ apollo: {
25
+ $subscribe: {
26
+ foo: {
27
+ // ...
28
+ },
29
+ bar: {
30
+ // This isn't defined in data.
31
+ // ...
32
+ },
33
+ },
34
+ qux: {
35
+ // ...
36
+ },
37
+ bbq: {
38
+ // This isn't defined in data.
39
+ // ...
40
+ },
41
+ },
42
+ render(h) {
43
+ return h();
44
+ },
45
+ };
46
+ </script>
47
+ ```
48
+
49
+ ### Examples of **correct** code for this rule
50
+
51
+ ```html
52
+ <script>
53
+ export default {
54
+ data() {
55
+ return {
56
+ foo: '',
57
+ qux: undefined,
58
+ bar: null,
59
+ bbq: [],
60
+ };
61
+ },
62
+ apollo: {
63
+ $subscribe: {
64
+ foo: {
65
+ // ...
66
+ },
67
+ bar: {
68
+ // ...
69
+ },
70
+ },
71
+ qux: {
72
+ // ...
73
+ },
74
+ bbq: {
75
+ // ...
76
+ },
77
+ },
78
+ render(h) {
79
+ return h();
80
+ },
81
+ };
82
+ </script>
83
+ ```
84
+
85
+ ## Options
86
+
87
+ Nothing
88
+
89
+ ## Related rules
90
+
91
+ Nothing
92
+
93
+ ## When Not To Use It
94
+
95
+ If you don't use VueApollo with the Options API, don't use this rule.
@@ -0,0 +1,45 @@
1
+ # @gitlab/vue-tailwind
2
+
3
+ This rule enforces the same conventions as [tailwind](./tailwind.md) in Vue templates.
4
+
5
+ ## Rule Details
6
+
7
+ ### Examples of **incorrect** code for this rule
8
+
9
+ ```html
10
+ <!-- Using interpolation to build a utility in a string literal -->
11
+ <template>
12
+ <span :class="'gl-bg-red-' + color"></span>
13
+ </template>
14
+
15
+ <!-- Using interpolation to build a utility in a template literal -->
16
+ <template>
17
+ <span :class="`gl-border-top gl-bg-red-${color} gl-text-gray-400`"></span>
18
+ </template>
19
+ ```
20
+
21
+ ### Examples of **correct** code for this rule
22
+
23
+ ```html
24
+ <!-- Utilities declared as a string literal without interpolation -->
25
+ <template>
26
+ <span class="gl-bg-red-800"></span>
27
+ </template>
28
+
29
+ <!-- Utilities declared as a template literal without interpolation -->
30
+ <template>
31
+ <span :class="`gl-bg-red-800`"></span>
32
+ </template>
33
+ ```
34
+
35
+ ## Options
36
+
37
+ No options.
38
+
39
+ ## Related rules
40
+
41
+ - [tailwind](./tailwind.md)
42
+
43
+ ## When Not To Use It
44
+
45
+ If the codebase doesn't leverage Tailwind CSS, keep this rule disabled.
package/docs/rules.md CHANGED
@@ -8,13 +8,16 @@ Available rules:
8
8
  - [no-runtime-template-compiler](./rules/no-runtime-template-compiler.md): Disallow components which rely on a runtime template compiler.
9
9
  - [require-i18n-strings](./rules/require-i18n-strings.md): Detect a string which has been hard coded and requires externalization.
10
10
  - [require-valid-i18n-helpers](./rules/require-valid-i18n-helpers.md): Enforces valid usage of translation helpers in JavaScript.
11
+ - [tailwind](./rules/tailwind.md): Ensures Tailwind CSS is used according to our internal guidelines.
11
12
  - [vtu-no-explicit-wrapper-destroy](./rules/vtu-no-explicit-wrapper-destroy.md): Prevents redundant destroy calls and null asignment
12
13
  - [vtu-no-wrapper-vm](./rules/vtu-no-wrapper-vm.md): Prevent direct access to `vm` internals for `@vue/test-utils` wrappers.
13
14
  - [vue-no-data-toggle](./rules/vue-no-data-toggle.md): Restrict the use of `data-toggle` bootstrap behaviors within Vue templates
14
15
  - [vue-no-new-non-primitive-in-template](./rules/vue-no-new-non-primitive-in-template.md): Prevents non-primitive values from being declared in templates
16
+ - [vue-no-undef-apollo-properties](./rules/vue-no-undef-apollo-properties.md): Require Apollo query properties to be initialized in component data.
15
17
  - [vue-prefer-dollar-scopedslots](./rules/vue-prefer-dollar-scopedslots.md): Prefer $scopedSlots over $slots for Vue 2.x.
16
18
  - [vue-require-i18n-attribute-strings](./rules/vue-require-i18n-attribute-strings.md): Detect non externalized strings in vue `<template>` attributes
17
19
  - [vue-require-i18n-strings](./rules/vue-require-i18n-strings.md): enforce no bare strings in vue `<template>`
18
20
  - [vue-require-required-key](./rules/vue-require-required-key.md): Require the required key to be set
19
21
  - [vue-require-valid-i18n-helpers](./rules/vue-require-valid-i18n-helpers.md): Enforces valid usage of translation helpers in Vue templates.
20
22
  - [vue-slot-name-casing](./rules/vue-slot-name-casing.md): enforce specific casing for slot naming style in template
23
+ - [vue-tailwind](./rules/vue-tailwind.md): Ensures Tailwind CSS is used according to our internal guidelines in Vue templates.
@@ -89,6 +89,7 @@ module.exports = {
89
89
  groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
90
90
  },
91
91
  ],
92
+ // https://docs.gitlab.com/ee/development/fe_guide/style/javascript.html#limit-number-of-parameters
92
93
  'max-params': ['error', { max: 3 }]
93
94
  },
94
95
  };
@@ -37,6 +37,7 @@ const tsConfig = {
37
37
  reportUnusedDisableDirectives: true,
38
38
  rules: {
39
39
  semi: [2, 'always'],
40
+ 'max-params': 'off',
40
41
  'no-throw-literal': 'off',
41
42
  'no-shadow': 'off',
42
43
  'no-empty-function': 'off',
package/lib/index.js CHANGED
@@ -7,23 +7,26 @@ module.exports = {
7
7
  'no-runtime-template-compiler': require('./rules/no-runtime-template-compiler.js'),
8
8
  'require-i18n-strings': require('./rules/require-i18n-strings.js'),
9
9
  'require-valid-i18n-helpers': require('./rules/require-valid-i18n-helpers.js'),
10
+ tailwind: require('./rules/tailwind.js'),
10
11
  'vtu-no-explicit-wrapper-destroy': require('./rules/vtu-no-explicit-wrapper-destroy.js'),
11
12
  'vtu-no-wrapper-vm': require('./rules/vtu-no-wrapper-vm.js'),
12
13
  'vue-no-data-toggle': require('./rules/vue-no-data-toggle.js'),
13
14
  'vue-no-new-non-primitive-in-template': require('./rules/vue-no-new-non-primitive-in-template.js'),
15
+ 'vue-no-undef-apollo-properties': require('./rules/vue-no-undef-apollo-properties.js'),
14
16
  'vue-prefer-dollar-scopedslots': require('./rules/vue-prefer-dollar-scopedslots.js'),
15
17
  'vue-require-i18n-attribute-strings': require('./rules/vue-require-i18n-attribute-strings.js'),
16
18
  'vue-require-i18n-strings': require('./rules/vue-require-i18n-strings.js'),
17
19
  'vue-require-required-key': require('./rules/vue-require-required-key.js'),
18
20
  'vue-require-valid-i18n-helpers': require('./rules/vue-require-valid-i18n-helpers.js'),
19
21
  'vue-slot-name-casing': require('./rules/vue-slot-name-casing.js'),
22
+ 'vue-tailwind': require('./rules/vue-tailwind.js'),
20
23
  },
21
24
  configs: {
22
- default: require('./configs/default.js'),
23
- typescript: require('./configs/typescript.js'),
24
25
  base: require('./configs/base.js'),
25
- vue: require('./configs/vue.js'),
26
+ default: require('./configs/default.js'),
26
27
  i18n: require('./configs/i18n.js'),
27
28
  jest: require('./configs/jest.js'),
29
+ typescript: require('./configs/typescript.js'),
30
+ vue: require('./configs/vue.js'),
28
31
  },
29
32
  };
@@ -0,0 +1,30 @@
1
+ // ------------------------------------------------------------------------------
2
+ // Requirements
3
+ // ------------------------------------------------------------------------------
4
+ const { DOCS_BASE_URL } = require('../constants');
5
+ const { validateInterpolatedUtils } = require('../utils/tailwind-utils');
6
+
7
+ // ------------------------------------------------------------------------------
8
+ // Rule Definition
9
+ // ------------------------------------------------------------------------------
10
+
11
+ module.exports = {
12
+ meta: {
13
+ type: 'error',
14
+ docs: {
15
+ description: 'Ensures Tailwind CSS is used according to our internal guidelines.',
16
+ category: 'css',
17
+ url: DOCS_BASE_URL + '/tailwind.md',
18
+ },
19
+ },
20
+ create(context) {
21
+ return {
22
+ BinaryExpression(node) {
23
+ validateInterpolatedUtils(context, node);
24
+ },
25
+ TemplateLiteral(node) {
26
+ validateInterpolatedUtils(context, node);
27
+ },
28
+ };
29
+ },
30
+ };
@@ -0,0 +1,102 @@
1
+ // ------------------------------------------------------------------------------
2
+ // Requirements
3
+ // ------------------------------------------------------------------------------
4
+ const { DOCS_BASE_URL } = require('../constants');
5
+ const utils = require('eslint-plugin-vue/lib/utils');
6
+
7
+ // ------------------------------------------------------------------------------
8
+ // Helpers
9
+ // ------------------------------------------------------------------------------
10
+
11
+ const DOLLAR_SUBSCRIBE = '$subscribe';
12
+
13
+ const VUE_APOLLO_SPECIAL_OPTIONS = new Set([
14
+ '$skipAll',
15
+ '$skipAllQueries',
16
+ '$skipAllSubscriptions',
17
+ '$deep',
18
+ '$error',
19
+ '$query',
20
+
21
+ // This one isn't special in the same way as the rest, but the normal code
22
+ // paths below need to be skipped for this case as well. It is handled in a
23
+ // separate code path.
24
+ DOLLAR_SUBSCRIBE,
25
+ ]);
26
+
27
+ function findProperty(objectExpression, name) {
28
+ return objectExpression.properties.find(
29
+ (p) =>
30
+ p.type === 'Property' &&
31
+ utils.getStaticPropertyName(p) === name &&
32
+ p.value.type === 'ObjectExpression',
33
+ );
34
+ }
35
+
36
+ function getDataKeys(componentObject) {
37
+ return new Set(
38
+ Array.from(utils.iterateProperties(componentObject, new Set(['data']))).map(({ name }) => name),
39
+ );
40
+ }
41
+
42
+ function* getApolloKeyNodeWrappers(componentObject) {
43
+ const apolloNode = findProperty(componentObject, 'apollo');
44
+
45
+ if (!apolloNode) return;
46
+
47
+ for (const nodeWrapper of utils.iterateObjectExpression(apolloNode.value)) {
48
+ if (VUE_APOLLO_SPECIAL_OPTIONS.has(nodeWrapper.name)) {
49
+ // Future-proofing against VueApollo 4's special options:
50
+ // https://apollo.vuejs.org/guide-option/special-options.html#special-options
51
+ continue;
52
+ }
53
+
54
+ yield nodeWrapper;
55
+ }
56
+
57
+ const subscribeNode = findProperty(apolloNode.value, DOLLAR_SUBSCRIBE);
58
+
59
+ if (subscribeNode) {
60
+ yield* utils.iterateObjectExpression(subscribeNode.value);
61
+ }
62
+ }
63
+
64
+ function* getApolloKeyNodes(componentObject) {
65
+ for (const nodeWrapper of getApolloKeyNodeWrappers(componentObject)) {
66
+ yield nodeWrapper.node;
67
+ }
68
+ }
69
+
70
+ // ------------------------------------------------------------------------------
71
+ // Rule Definition
72
+ // ------------------------------------------------------------------------------
73
+
74
+ module.exports = {
75
+ meta: {
76
+ type: 'error',
77
+ docs: {
78
+ description: 'Require Apollo query properties to be initialized in component data.',
79
+ url: DOCS_BASE_URL + '/vue-no-undef-apollo-properties.md',
80
+ },
81
+ messages: {
82
+ noInitialValue: 'Apollo query property {{name}} is not initialized in component data.',
83
+ },
84
+ },
85
+ create(context) {
86
+ return utils.executeOnVue(context, (node) => {
87
+ const dataKeys = getDataKeys(node);
88
+
89
+ for (const apolloKeyNode of getApolloKeyNodes(node)) {
90
+ if (!dataKeys.has(apolloKeyNode.name)) {
91
+ context.report({
92
+ node: apolloKeyNode,
93
+ messageId: 'noInitialValue',
94
+ data: {
95
+ name: apolloKeyNode.name,
96
+ },
97
+ });
98
+ }
99
+ }
100
+ });
101
+ },
102
+ };
@@ -0,0 +1,32 @@
1
+
2
+ // ------------------------------------------------------------------------------
3
+ // Requirements
4
+ // ------------------------------------------------------------------------------
5
+ const { DOCS_BASE_URL } = require('../constants');
6
+ const { defineTemplateBodyVisitor } = require('../utils/index');
7
+ const { validateInterpolatedUtils } = require('../utils/tailwind-utils');
8
+
9
+ // ------------------------------------------------------------------------------
10
+ // Rule Definition
11
+ // ------------------------------------------------------------------------------
12
+
13
+ module.exports = {
14
+ meta: {
15
+ type: 'error',
16
+ docs: {
17
+ description: 'Ensures Tailwind CSS is used according to our internal guidelines in Vue templates.',
18
+ category: 'css',
19
+ url: DOCS_BASE_URL + '/vue-tailwind.md',
20
+ },
21
+ },
22
+ create(context) {
23
+ return defineTemplateBodyVisitor(context, {
24
+ BinaryExpression(node) {
25
+ validateInterpolatedUtils(context, node)
26
+ },
27
+ TemplateLiteral(node) {
28
+ validateInterpolatedUtils(context, node)
29
+ },
30
+ });
31
+ },
32
+ };
@@ -0,0 +1,35 @@
1
+ const INTERPOLATION_PATTERN = /gl-[a-z0-9-]*$/i;
2
+ const INTERPOLATED_UTIL_ERROR =
3
+ 'You are building a CSS utility class name using string interpolation which is forbidden because Tailwind CSS needs fully qualified names to properly generate the utilities we use.';
4
+ const INTERPOLATED_UTILS_VALIDATORS = {
5
+ BinaryExpression: binaryExpressionHasInterpolatedUtils,
6
+ TemplateLiteral: templateLiteralHasInterpolatedUtils,
7
+ };
8
+
9
+ function binaryExpressionHasInterpolatedUtils(node) {
10
+ return node.operator === '+' && INTERPOLATION_PATTERN.test(node.left.value);
11
+ }
12
+
13
+ function templateLiteralHasInterpolatedUtils(node) {
14
+ return node.quasis.some((quasi) => {
15
+ return quasi.tail === false && INTERPOLATION_PATTERN.test(quasi.value.raw);
16
+ });
17
+ }
18
+
19
+ function validateInterpolatedUtils(context, node) {
20
+ if (INTERPOLATED_UTILS_VALIDATORS[node.type] === undefined) {
21
+ return;
22
+ }
23
+
24
+ if (INTERPOLATED_UTILS_VALIDATORS[node.type](node)) {
25
+ context.report({
26
+ node,
27
+ message: INTERPOLATED_UTIL_ERROR,
28
+ });
29
+ }
30
+ }
31
+
32
+ module.exports = {
33
+ validateInterpolatedUtils,
34
+ INTERPOLATED_UTIL_ERROR,
35
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/eslint-plugin",
3
- "version": "20.0.0",
3
+ "version": "20.2.0",
4
4
  "description": "GitLab package for our custom eslint rules",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {