From b299198e1eb5e1f26d5267f4a64944e600086d6b Mon Sep 17 00:00:00 2001
From: Filipa Lacerda <filipa@gitlab.com>
Date: Wed, 3 Jan 2018 23:14:55 +0000
Subject: [PATCH] Adds `eslint-plugin-vue`, fixes linter errors and adds docs

---
 .eslintrc                                     |   9 +-
 app/assets/javascripts/blob/notebook/index.js |  76 +++----
 app/assets/javascripts/blob/pdf/index.js      |   6 +-
 .../javascripts/boards/boards_bundle.js       |  18 +-
 .../clusters/components/applications.vue      |  32 +--
 app/assets/javascripts/commit/image_file.js   | 194 +++++++++---------
 .../cycle_analytics/cycle_analytics_bundle.js |  20 +-
 app/assets/javascripts/deploy_keys/index.js   |   6 +-
 .../components/environment_item.vue           |   3 +-
 .../filtered_search/recent_searches_root.js   |   6 +-
 .../groups/components/group_folder.vue        |   6 +-
 .../groups/components/item_stats.vue          |   7 +-
 .../ide/components/repo_commit_section.vue    |   4 +-
 app/assets/javascripts/ide/index.js           |  10 +-
 .../issue_show/components/description.vue     |   6 +-
 .../javascripts/jobs/job_details_bundle.js    |  12 +-
 .../merge_conflicts/merge_conflicts_bundle.js |   2 +-
 .../monitoring/components/empty_state.vue     |   6 +-
 .../notes/components/comment_form.vue         |  14 +-
 .../notes/components/noteable_discussion.vue  |   3 +-
 .../graph/dropdown_job_component.vue          |   3 +-
 .../pipelines/components/pipeline_url.vue     |  12 +-
 .../pipelines/components/stage.vue            |   4 +-
 .../pipelines/pipeline_details_bundle.js      |  12 +-
 .../javascripts/pipelines/pipelines_bundle.js |   6 +-
 .../components/delete_account_modal.vue       |   3 +-
 .../confidential_issue_sidebar.vue            |   5 +-
 .../components/lock/lock_issue_sidebar.vue    |   3 +-
 doc/development/fe_guide/style_guide_js.md    |  14 +-
 package.json                                  |   1 +
 spec/javascripts/boards/issue_card_spec.js    |   6 +-
 .../components/markdown/field_spec.js         |   6 +-
 yarn.lock                                     |  44 ++++
 33 files changed, 333 insertions(+), 226 deletions(-)

diff --git a/.eslintrc b/.eslintrc
index 44ad6a4896c..a419dc521e8 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -4,7 +4,10 @@
     "browser": true,
     "es6": true
   },
-  "extends": "airbnb-base",
+  "extends": [
+    "airbnb-base",
+    "plugin:vue/recommended"
+  ],
   "globals": {
     "__webpack_public_path__": true,
     "_": false,
@@ -12,7 +15,9 @@
     "gon": false,
     "localStorage": false
   },
-  "parser": "babel-eslint",
+  "parserOptions": {
+    "parser": "babel-eslint"
+  },
   "plugins": [
     "filenames",
     "import",
diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js
index 57b031956e8..6f1350e80fc 100644
--- a/app/assets/javascripts/blob/notebook/index.js
+++ b/app/assets/javascripts/blob/notebook/index.js
@@ -8,6 +8,9 @@ export default () => {
 
   new Vue({
     el,
+    components: {
+      notebookLab,
+    },
     data() {
       return {
         error: false,
@@ -16,8 +19,41 @@ export default () => {
         json: {},
       };
     },
-    components: {
-      notebookLab,
+    mounted() {
+      if (gon.katex_css_url) {
+        const katexStyles = document.createElement('link');
+        katexStyles.setAttribute('rel', 'stylesheet');
+        katexStyles.setAttribute('href', gon.katex_css_url);
+        document.head.appendChild(katexStyles);
+      }
+
+      if (gon.katex_js_url) {
+        const katexScript = document.createElement('script');
+        katexScript.addEventListener('load', () => {
+          this.loadFile();
+        });
+        katexScript.setAttribute('src', gon.katex_js_url);
+        document.head.appendChild(katexScript);
+      } else {
+        this.loadFile();
+      }
+    },
+    methods: {
+      loadFile() {
+        axios.get(el.dataset.endpoint)
+          .then(res => res.data)
+          .then((data) => {
+            this.json = data;
+            this.loading = false;
+          })
+          .catch((e) => {
+            if (e.status !== 200) {
+              this.loadError = true;
+            }
+
+            this.error = true;
+          });
+      },
     },
     template: `
       <div class="container-fluid md prepend-top-default append-bottom-default">
@@ -46,41 +82,5 @@ export default () => {
         </p>
       </div>
     `,
-    methods: {
-      loadFile() {
-        axios.get(el.dataset.endpoint)
-          .then(res => res.data)
-          .then((data) => {
-            this.json = data;
-            this.loading = false;
-          })
-          .catch((e) => {
-            if (e.status !== 200) {
-              this.loadError = true;
-            }
-
-            this.error = true;
-          });
-      },
-    },
-    mounted() {
-      if (gon.katex_css_url) {
-        const katexStyles = document.createElement('link');
-        katexStyles.setAttribute('rel', 'stylesheet');
-        katexStyles.setAttribute('href', gon.katex_css_url);
-        document.head.appendChild(katexStyles);
-      }
-
-      if (gon.katex_js_url) {
-        const katexScript = document.createElement('script');
-        katexScript.addEventListener('load', () => {
-          this.loadFile();
-        });
-        katexScript.setAttribute('src', gon.katex_js_url);
-        document.head.appendChild(katexScript);
-      } else {
-        this.loadFile();
-      }
-    },
   });
 };
diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js
index 7109f356540..70136cc4087 100644
--- a/app/assets/javascripts/blob/pdf/index.js
+++ b/app/assets/javascripts/blob/pdf/index.js
@@ -7,6 +7,9 @@ export default () => {
 
   return new Vue({
     el,
+    components: {
+      pdfLab,
+    },
     data() {
       return {
         error: false,
@@ -15,9 +18,6 @@ export default () => {
         pdf: el.dataset.endpoint,
       };
     },
-    components: {
-      pdfLab,
-    },
     methods: {
       onLoad() {
         this.loading = false;
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index 679c883cdcf..90166b3d3d1 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -171,19 +171,14 @@ $(() => {
   });
 
   gl.IssueBoardsModalAddBtn = new Vue({
-    mixins: [gl.issueBoards.ModalMixins],
     el: document.getElementById('js-add-issues-btn'),
+    mixins: [gl.issueBoards.ModalMixins],
     data() {
       return {
         modal: ModalStore.store,
         store: Store.state,
       };
     },
-    watch: {
-      disabled() {
-        this.updateTooltip();
-      },
-    },
     computed: {
       disabled() {
         if (!this.store) {
@@ -199,6 +194,14 @@ $(() => {
         return '';
       },
     },
+    watch: {
+      disabled() {
+        this.updateTooltip();
+      },
+    },
+    mounted() {
+      this.updateTooltip();
+    },
     methods: {
       updateTooltip() {
         const $tooltip = $(this.$refs.addIssuesButton);
@@ -217,9 +220,6 @@ $(() => {
         }
       },
     },
-    mounted() {
-      this.updateTooltip();
-    },
     template: `
       <div class="board-extra-actions">
         <button
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index cd58b88db69..1b0bbffe37e 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -21,7 +21,9 @@ export default {
   computed: {
     generalApplicationDescription() {
       return sprintf(
-        _.escape(s__('ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}')), {
+        _.escape(s__(`ClusterIntegration|Install applications on your cluster.
+Read more about %{helpLink}`)),
+        {
           helpLink: `<a href="${this.helpPath}">
             ${_.escape(s__('ClusterIntegration|installing applications'))}
           </a>`,
@@ -43,12 +45,15 @@ export default {
       ));
 
       const extraCostParagraph = sprintf(
-        _.escape(s__('ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}')), {
-          boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
-          pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
-            ${_.escape(s__('ClusterIntegration|GKE pricing'))}
-          </a>`,
-        },
+        _.escape(s__(`ClusterIntegration|%{boldNotice} This will add some
+extra resources like a load balancer,
+which incur additional costs. See %{pricingLink}`)),
+          {
+            boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
+            pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="_blank" rel="noopener noreferrer">
+              ${_.escape(s__('ClusterIntegration|GKE pricing'))}
+            </a>`,
+          },
         false,
       );
 
@@ -69,11 +74,14 @@ export default {
     },
     prometheusDescription() {
       return sprintf(
-        _.escape(s__('ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications.')), {
-          gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html", target="_blank" rel="noopener noreferrer">
-            ${_.escape(s__('ClusterIntegration|Gitlab Integration'))}
-          </a>`,
-        },
+        _.escape(s__(`ClusterIntegration|Prometheus is an open-source monitoring system
+with %{gitlabIntegrationLink} to monitor deployed applications.`)),
+          {
+            gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
+target="_blank" rel="noopener noreferrer">
+              ${_.escape(s__('ClusterIntegration|Gitlab Integration'))}
+            </a>`,
+          },
         false,
       );
     },
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index b6a0ece7907..485d81882d2 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -23,6 +23,103 @@ export default class ImageFile {
         });
       };
     })(this));
+
+    this.views = {
+      'two-up': function() {
+        return $('.two-up.view .wrap', this.file).each((function(_this) {
+          return function(index, wrap) {
+            $('img', wrap).each(function() {
+              var currentWidth;
+              currentWidth = $(this).width();
+              if (currentWidth > availWidth / 2) {
+                return $(this).width(availWidth / 2);
+              }
+            });
+            return _this.requestImageInfo($('img', wrap), function(width, height) {
+              $('.image-info .meta-width', wrap).text(width + "px");
+              $('.image-info .meta-height', wrap).text(height + "px");
+              return $('.image-info', wrap).removeClass('hide');
+            });
+          };
+        })(this));
+      },
+      'swipe': function() {
+        var maxHeight, maxWidth;
+        maxWidth = 0;
+        maxHeight = 0;
+        return $('.swipe.view', this.file).each((function(_this) {
+          return function(index, view) {
+            var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
+            ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
+            $swipeFrame = $('.swipe-frame', view);
+            $swipeWrap = $('.swipe-wrap', view);
+            $swipeBar = $('.swipe-bar', view);
+
+            $swipeFrame.css({
+              width: maxWidth + 16,
+              height: maxHeight + 28
+            });
+            $swipeWrap.css({
+              width: maxWidth + 1,
+              height: maxHeight + 2
+            });
+            // Set swipeBar left position to match image frame
+            $swipeBar.css({
+              left: 1
+            });
+
+            wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
+
+            _this.initDraggable($swipeBar, wrapPadding, function(e, left) {
+              if (left > 0 && left < $swipeFrame.width() - (wrapPadding * 2)) {
+                $swipeWrap.width((maxWidth + 1) - left);
+                $swipeBar.css('left', left);
+              }
+            });
+          };
+        })(this));
+      },
+      'onion-skin': function() {
+        var dragTrackWidth, maxHeight, maxWidth;
+        maxWidth = 0;
+        maxHeight = 0;
+        dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width();
+        return $('.onion-skin.view', this.file).each((function(_this) {
+          return function(index, view) {
+            var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false;
+            ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
+            $frame = $('.onion-skin-frame', view);
+            $frameAdded = $('.frame.added', view);
+            $track = $('.drag-track', view);
+            $dragger = $('.dragger', $track);
+
+            $frame.css({
+              width: maxWidth + 16,
+              height: maxHeight + 28
+            });
+            $('.swipe-wrap', view).css({
+              width: maxWidth + 1,
+              height: maxHeight + 2
+            });
+            $dragger.css({
+              left: dragTrackWidth
+            });
+
+            $frameAdded.css('opacity', 1);
+            framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
+
+            _this.initDraggable($dragger, framePadding, function(e, left) {
+              var opacity = left / dragTrackWidth;
+
+              if (opacity >= 0 && opacity <= 1) {
+                $dragger.css('left', left);
+                $frameAdded.css('opacity', opacity);
+              }
+            });
+          };
+        })(this));
+      }
+    };
   }
 
   initViewModes() {
@@ -95,103 +192,6 @@ export default class ImageFile {
     return [maxWidth, maxHeight];
   }
 
-  views = {
-    'two-up': function() {
-      return $('.two-up.view .wrap', this.file).each((function(_this) {
-        return function(index, wrap) {
-          $('img', wrap).each(function() {
-            var currentWidth;
-            currentWidth = $(this).width();
-            if (currentWidth > availWidth / 2) {
-              return $(this).width(availWidth / 2);
-            }
-          });
-          return _this.requestImageInfo($('img', wrap), function(width, height) {
-            $('.image-info .meta-width', wrap).text(width + "px");
-            $('.image-info .meta-height', wrap).text(height + "px");
-            return $('.image-info', wrap).removeClass('hide');
-          });
-        };
-      })(this));
-    },
-    'swipe': function() {
-      var maxHeight, maxWidth;
-      maxWidth = 0;
-      maxHeight = 0;
-      return $('.swipe.view', this.file).each((function(_this) {
-        return function(index, view) {
-          var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
-          ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
-          $swipeFrame = $('.swipe-frame', view);
-          $swipeWrap = $('.swipe-wrap', view);
-          $swipeBar = $('.swipe-bar', view);
-
-          $swipeFrame.css({
-            width: maxWidth + 16,
-            height: maxHeight + 28
-          });
-          $swipeWrap.css({
-            width: maxWidth + 1,
-            height: maxHeight + 2
-          });
-          // Set swipeBar left position to match image frame
-          $swipeBar.css({
-            left: 1
-          });
-
-          wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
-
-          _this.initDraggable($swipeBar, wrapPadding, function(e, left) {
-            if (left > 0 && left < $swipeFrame.width() - (wrapPadding * 2)) {
-              $swipeWrap.width((maxWidth + 1) - left);
-              $swipeBar.css('left', left);
-            }
-          });
-        };
-      })(this));
-    },
-    'onion-skin': function() {
-      var dragTrackWidth, maxHeight, maxWidth;
-      maxWidth = 0;
-      maxHeight = 0;
-      dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width();
-      return $('.onion-skin.view', this.file).each((function(_this) {
-        return function(index, view) {
-          var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false;
-          ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
-          $frame = $('.onion-skin-frame', view);
-          $frameAdded = $('.frame.added', view);
-          $track = $('.drag-track', view);
-          $dragger = $('.dragger', $track);
-
-          $frame.css({
-            width: maxWidth + 16,
-            height: maxHeight + 28
-          });
-          $('.swipe-wrap', view).css({
-            width: maxWidth + 1,
-            height: maxHeight + 2
-          });
-          $dragger.css({
-            left: dragTrackWidth
-          });
-
-          $frameAdded.css('opacity', 1);
-          framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
-
-          _this.initDraggable($dragger, framePadding, function(e, left) {
-            var opacity = left / dragTrackWidth;
-
-            if (opacity >= 0 && opacity <= 1) {
-              $dragger.css('left', left);
-              $frameAdded.css('opacity', opacity);
-            }
-          });
-        };
-      })(this));
-    }
-  }
-
   requestImageInfo(img, callback) {
     const domImg = img.get(0);
     if (domImg) {
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 49bb6c52180..034f2923b3b 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -20,6 +20,16 @@ $(() => {
   gl.cycleAnalyticsApp = new Vue({
     el: '#cycle-analytics',
     name: 'CycleAnalytics',
+    components: {
+      banner,
+      'stage-issue-component': stageComponent,
+      'stage-plan-component': stagePlanComponent,
+      'stage-code-component': stageCodeComponent,
+      'stage-test-component': stageTestComponent,
+      'stage-review-component': stageReviewComponent,
+      'stage-staging-component': stageStagingComponent,
+      'stage-production-component': stageComponent,
+    },
     data() {
       const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
       const cycleAnalyticsService = new CycleAnalyticsService({
@@ -43,16 +53,6 @@ $(() => {
         return this.store.currentActiveStage();
       },
     },
-    components: {
-      banner,
-      'stage-issue-component': stageComponent,
-      'stage-plan-component': stagePlanComponent,
-      'stage-code-component': stageCodeComponent,
-      'stage-test-component': stageTestComponent,
-      'stage-review-component': stageReviewComponent,
-      'stage-staging-component': stageStagingComponent,
-      'stage-production-component': stageComponent,
-    },
     created() {
       this.fetchCycleAnalyticsData();
     },
diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js
index a5f232f950a..ca8798facc9 100644
--- a/app/assets/javascripts/deploy_keys/index.js
+++ b/app/assets/javascripts/deploy_keys/index.js
@@ -3,14 +3,14 @@ import deployKeysApp from './components/app.vue';
 
 document.addEventListener('DOMContentLoaded', () => new Vue({
   el: document.getElementById('js-deploy-keys'),
+  components: {
+    deployKeysApp,
+  },
   data() {
     return {
       endpoint: this.$options.el.dataset.endpoint,
     };
   },
-  components: {
-    deployKeysApp,
-  },
   render(createElement) {
     return createElement('deploy-keys-app', {
       props: {
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 2f0e397aa45..f647eed6952 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -287,7 +287,8 @@ export default {
       if (this.model &&
         this.model.last_deployment &&
         this.model.last_deployment.deployable) {
-        return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`;
+        const deployable = this.model.last_deployment.deployable;
+        return `${deployable.name} #${deployable.id}`;
       }
       return '';
     },
diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js
index 27e49d4fb96..c99ed63c4af 100644
--- a/app/assets/javascripts/filtered_search/recent_searches_root.js
+++ b/app/assets/javascripts/filtered_search/recent_searches_root.js
@@ -32,6 +32,9 @@ class RecentSearchesRoot {
     const state = this.store.state;
     this.vm = new Vue({
       el: this.wrapperElement,
+      components: {
+        'recent-searches-dropdown-content': RecentSearchesDropdownContent,
+      },
       data() { return state; },
       template: `
         <recent-searches-dropdown-content
@@ -40,9 +43,6 @@ class RecentSearchesRoot {
           :allowed-keys="allowedKeys"
           />
       `,
-      components: {
-        'recent-searches-dropdown-content': RecentSearchesDropdownContent,
-      },
     });
   }
 
diff --git a/app/assets/javascripts/groups/components/group_folder.vue b/app/assets/javascripts/groups/components/group_folder.vue
index e60221fa08d..1ff984d02e2 100644
--- a/app/assets/javascripts/groups/components/group_folder.vue
+++ b/app/assets/javascripts/groups/components/group_folder.vue
@@ -20,7 +20,11 @@ export default {
       return this.parentGroup.childrenCount > MAX_CHILDREN_COUNT;
     },
     moreChildrenStats() {
-      return n__('One more item', '%d more items', this.parentGroup.childrenCount - this.parentGroup.children.length);
+      return n__(
+        'One more item',
+        '%d more items',
+        this.parentGroup.childrenCount - this.parentGroup.children.length
+      );
     },
   },
 };
diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue
index 9f8ac138fc3..d9bc0588908 100644
--- a/app/assets/javascripts/groups/components/item_stats.vue
+++ b/app/assets/javascripts/groups/components/item_stats.vue
@@ -1,6 +1,11 @@
 <script>
 import tooltip from '../../vue_shared/directives/tooltip';
-import { ITEM_TYPE, VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE, PROJECT_VISIBILITY_TYPE } from '../constants';
+import {
+  ITEM_TYPE,
+  VISIBILITY_TYPE_ICON,
+  GROUP_VISIBILITY_TYPE,
+  PROJECT_VISIBILITY_TYPE,
+} from '../constants';
 
 export default {
   directives: {
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 470db2c9650..b1ec82f5209 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -49,7 +49,9 @@ export default {
       const createNewBranch = newBranch || this.startNewMR;
 
       const payload = {
-        branch: createNewBranch ? `${this.currentBranchId}-${new Date().getTime().toString()}` : this.currentBranchId,
+        branch: createNewBranch ?
+          `${this.currentBranchId}-${new Date().getTime().toString()}` :
+          this.currentBranchId,
         commit_message: this.commitMessage,
         actions: this.changedFiles.map(f => ({
           action: f.tempFile ? 'create' : 'update',
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index a96bd339f51..e7b0794596b 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -18,11 +18,6 @@ function initIde(el) {
     components: {
       ide,
     },
-    methods: {
-      ...mapActions([
-        'setInitialData',
-      ]),
-    },
     created() {
       const data = el.dataset;
 
@@ -39,6 +34,11 @@ function initIde(el) {
         isInitialRoot: convertPermissionToBoolean(data.root),
       });
     },
+    methods: {
+      ...mapActions([
+        'setInitialData',
+      ]),
+    },
     render(createElement) {
       return createElement('ide');
     },
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index c3f2bf130bb..317020f25eb 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -88,7 +88,11 @@
 
         if (taskRegexMatches) {
           $tasks.text(this.taskStatus);
-          $tasksShort.text(`${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ? 's' : ''}`);
+          $tasksShort.text(
+            `${taskRegexMatches[1]}/${taskRegexMatches[2]} task${taskRegexMatches[2] > 1 ?
+            's' :
+            ''}`
+          );
         } else {
           $tasks.text('');
           $tasksShort.text('');
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
index baaf5641200..db53b04de0e 100644
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -13,14 +13,14 @@ document.addEventListener('DOMContentLoaded', () => {
   // eslint-disable-next-line no-new
   new Vue({
     el: '#js-build-header-vue',
+    components: {
+      jobHeader,
+    },
     data() {
       return {
         mediator,
       };
     },
-    components: {
-      jobHeader,
-    },
     mounted() {
       this.mediator.initBuildClass();
     },
@@ -38,14 +38,14 @@ document.addEventListener('DOMContentLoaded', () => {
   // eslint-disable-next-line
   new Vue({
     el: '#js-details-block-vue',
+    components: {
+      detailsBlock,
+    },
     data() {
       return {
         mediator,
       };
     },
-    components: {
-      detailsBlock,
-    },
     render(createElement) {
       return createElement('details-block', {
         props: {
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index 94561d6b7c3..792b7523889 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -25,12 +25,12 @@ $(() => {
 
   gl.MergeConflictsResolverApp = new Vue({
     el: '#conflicts',
-    data: mergeConflictsStore.state,
     components: {
       'diff-file-editor': gl.mergeConflicts.diffFileEditor,
       'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines,
       'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines
     },
+    data: mergeConflictsStore.state,
     computed: {
       conflictsCountText() { return mergeConflictsStore.getConflictsCountText(); },
       readyToCommit() { return mergeConflictsStore.isReadyToCommit(); },
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index a18164482a2..9df7094b6ab 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -33,13 +33,15 @@
           gettingStarted: {
             svgUrl: this.emptyGettingStartedSvgPath,
             title: 'Get started with performance monitoring',
-            description: 'Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.',
+            description: `Stay updated about the performance and health
+of your environment by configuring Prometheus to monitor your deployments.`,
             buttonText: 'Configure Prometheus',
           },
           loading: {
             svgUrl: this.emptyLoadingSvgPath,
             title: 'Waiting for performance data',
-            description: 'Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.',
+            description: `Creating graphs uses the data from the Prometheus server.
+If this takes a long time, ensure that data is available.`,
             buttonText: 'View documentation',
           },
           unableToConnect: {
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index e594377bc40..778db2f7132 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -65,7 +65,9 @@
         if (this.note.length) {
           const actionText = this.isIssueOpen ? 'close' : 'reopen';
 
-          return this.noteType === constants.COMMENT ? `Comment & ${actionText} issue` : `Start discussion & ${actionText} issue`;
+          return this.noteType === constants.COMMENT ?
+            `Comment & ${actionText} issue` :
+            `Start discussion & ${actionText} issue`;
         }
 
         return this.isIssueOpen ? 'Close issue' : 'Reopen issue';
@@ -159,7 +161,9 @@
             .catch(() => {
               this.isSubmitting = false;
               this.discard(false);
-              const msg = 'Your comment could not be submitted! Please check your network connection and try again.';
+              const msg =
+                `Your comment could not be submitted!
+Please check your network connection and try again.`;
               Flash(msg, 'alert', this.$el);
               this.note = noteData.data.note.note; // Restore textarea content.
               this.removePlaceholderNotes();
@@ -207,7 +211,11 @@
       },
       initAutoSave() {
         if (this.isLoggedIn) {
-          this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getNoteableData.id], 'issue');
+          this.autosave = new Autosave(
+            $(this.$refs.textarea),
+            ['Note', 'Issue', this.getNoteableData.id],
+            'issue',
+          );
         }
       },
       initTaskList() {
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 11e8f805635..873c4f1ff96 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -130,7 +130,8 @@
             this.removePlaceholderNotes();
             this.isReplying = true;
             this.$nextTick(() => {
-              const msg = 'Your comment could not be submitted! Please check your network connection and try again.';
+              const msg = `Your comment could not be submitted!
+Please check your network connection and try again.`;
               Flash(msg, 'alert', this.$el);
               this.$refs.noteForm.note = noteText;
               callback(err);
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
index 7006d05e7b2..933d21bf17b 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -59,7 +59,8 @@
      * target the click event of this component.
      */
       stopDropdownClickPropagation() {
-        $(this.$el.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
+        $(this.$el
+          .querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
           .on('click', (e) => {
             e.stopPropagation();
           });
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index 9da0aac50a1..1e2f45333a5 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -30,8 +30,16 @@
           html: true,
           trigger: 'focus',
           placement: 'top',
-          title: '<div class="autodevops-title">This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b></div>',
-          content: `<a class="autodevops-link" href="${this.autoDevopsHelpPath}" target="_blank" rel="noopener noreferrer nofollow">Learn more about Auto DevOps</a>`,
+          title: `<div class="autodevops-title">
+            This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b>
+          </div>`,
+          content: `<a
+            class="autodevops-link"
+            href="${this.autoDevopsHelpPath}"
+            target="_blank"
+            rel="noopener noreferrer nofollow">
+            Learn more about Auto DevOps
+          </a>`,
         };
       },
     },
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index ac9d9c901ca..021f271b267 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -116,7 +116,9 @@ export default {
 
   computed: {
     dropdownClass() {
-      return this.dropdownContent.length > 0 ? 'js-builds-dropdown-container' : 'js-builds-dropdown-loading';
+      return this.dropdownContent.length > 0 ?
+        'js-builds-dropdown-container' :
+        'js-builds-dropdown-loading';
     },
 
     triggerButtonClass() {
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 206023d4ddb..d88d280cb3f 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -15,14 +15,14 @@ document.addEventListener('DOMContentLoaded', () => {
   // eslint-disable-next-line
   new Vue({
     el: '#js-pipeline-graph-vue',
+    components: {
+      pipelineGraph,
+    },
     data() {
       return {
         mediator,
       };
     },
-    components: {
-      pipelineGraph,
-    },
     render(createElement) {
       return createElement('pipeline-graph', {
         props: {
@@ -36,14 +36,14 @@ document.addEventListener('DOMContentLoaded', () => {
   // eslint-disable-next-line
   new Vue({
     el: '#js-pipeline-header-vue',
+    components: {
+      pipelineHeader,
+    },
     data() {
       return {
         mediator,
       };
     },
-    components: {
-      pipelineHeader,
-    },
     created() {
       eventHub.$on('headerPostAction', this.postAction);
     },
diff --git a/app/assets/javascripts/pipelines/pipelines_bundle.js b/app/assets/javascripts/pipelines/pipelines_bundle.js
index 3e4b6eeb5bf..ab5596e70f0 100644
--- a/app/assets/javascripts/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/pipelines/pipelines_bundle.js
@@ -7,6 +7,9 @@ Vue.use(Translate);
 
 document.addEventListener('DOMContentLoaded', () => new Vue({
   el: '#pipelines-list-vue',
+  components: {
+    pipelinesComponent,
+  },
   data() {
     const store = new PipelinesStore();
 
@@ -14,9 +17,6 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
       store,
     };
   },
-  components: {
-    pipelinesComponent,
-  },
   render(createElement) {
     return createElement('pipelines-component', {
       props: {
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index 78be6b6e884..67ed7cc0cdf 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -51,7 +51,8 @@
       text() {
         return sprintf(
           s__(`Profiles|
-You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account.
+You are about to permanently delete %{yourAccount}, and all of the issues, merge requests,
+and groups linked to your account.
 Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
           {
             yourAccount: `<strong>${s__('Profiles|your account')}</strong>`,
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 6ee4d487c0b..80927529ffe 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -39,7 +39,10 @@ export default {
     updateConfidentialAttribute(confidential) {
       this.service.update('issue', { confidential })
         .then(() => location.reload())
-        .catch(() => new Flash('Something went wrong trying to change the confidentiality of this issue'));
+        .catch(() => {
+          Flash(`Something went wrong trying to
+change the confidentiality of this issue`);
+        });
     },
   },
 };
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index 04c3a96bf74..bded18996eb 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -54,7 +54,8 @@ export default {
         discussion_locked: locked,
       })
       .then(() => location.reload())
-      .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName}`)));
+      .catch(() => Flash(this.__(`Something went wrong trying to
+change the locked state of this ${this.issuableDisplayName}`)));
     },
   },
 };
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index 1cd66f27492..3c5d69e1f71 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -101,16 +101,16 @@ followed by any global declarations, then a blank newline prior to any imports o
     ```
 
     Import statements are following usual naming guidelines, for example object literals use camel case:
-    
+
     ```javascript
       // some_object file
       export default {
         key: 'value',
       };
-      
+
       // bad
       import ObjectLiteral from 'some_object';
-      
+
       // good
       import objectLiteral from 'some_object';
     ```
@@ -255,6 +255,10 @@ A forEach will cause side effects, it will be mutating the array being iterated.
 
 ### Vue.js
 
+#### `eslint-vue-plugin`
+We default to [eslint-vue-plugin][eslint-plugin-vue], with the `plugin:vue/recommended`.
+Please check this [rules][eslint-plugin-vue-rules] for more documentation.
+
 #### Basic Rules
 1. The service has it's own file
 1. The store has it's own file
@@ -513,8 +517,8 @@ On those a default key should not be provided.
   1. `props`
   1. `mixins`
   1. `directives`
-  1. `data`
   1. `components`
+  1. `data`
   1. `computedProps`
   1. `methods`
   1. `beforeCreate`
@@ -582,3 +586,5 @@ The goal of this accord is to make sure we are all on the same page.
 [eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc
 [eslint-this]: http://eslint.org/docs/rules/class-methods-use-this
 [eslint-new]: http://eslint.org/docs/rules/no-new
+[eslint-plugin-vue]: https://github.com/vuejs/eslint-plugin-vue
+[eslint-plugin-vue-rules]: https://github.com/vuejs/eslint-plugin-vue#bulb-rules
diff --git a/package.json b/package.json
index 8c3932dccfd..f7fa71c2be6 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
     "dropzone": "^4.2.0",
     "emoji-unicode-version": "^0.2.1",
     "eslint-plugin-html": "^2.0.1",
+    "eslint-plugin-vue": "^4.0.1",
     "exports-loader": "^0.6.4",
     "file-loader": "^0.11.1",
     "fuzzaldrin-plus": "^0.5.0",
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index 8ef221257be..278155c585e 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -45,6 +45,9 @@ describe('Issue card component', () => {
 
     component = new Vue({
       el: document.querySelector('.test-container'),
+      components: {
+        'issue-card': gl.issueBoards.IssueCardInner,
+      },
       data() {
         return {
           list,
@@ -53,9 +56,6 @@ describe('Issue card component', () => {
           rootPath: '/',
         };
       },
-      components: {
-        'issue-card': gl.issueBoards.IssueCardInner,
-      },
       template: `
         <issue-card
           :issue="issue"
diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js
index 24209be83fe..5f980bbf36c 100644
--- a/spec/javascripts/vue_shared/components/markdown/field_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js
@@ -12,14 +12,14 @@ describe('Markdown field component', () => {
 
   beforeEach((done) => {
     vm = new Vue({
+      components: {
+        fieldComponent,
+      },
       data() {
         return {
           text: 'testing\n123',
         };
       },
-      components: {
-        fieldComponent,
-      },
       template: `
         <field-component
           markdown-preview-path="/preview"
diff --git a/yarn.lock b/yarn.lock
index 381b1a243f8..7e6649db0c7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -97,6 +97,10 @@ acorn@^5.0.0, acorn@^5.0.3, acorn@^5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75"
 
+acorn@^5.2.1:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.3.0.tgz#7446d39459c54fb49a80e6ee6478149b940ec822"
+
 after@0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
@@ -2397,6 +2401,24 @@ eslint-plugin-promise@^3.5.0:
   version "3.5.0"
   resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.5.0.tgz#78fbb6ffe047201627569e85a6c5373af2a68fca"
 
+eslint-plugin-vue@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-4.0.1.tgz#afda92cfd7e7363b1fbdb1a772dd63359a9ce96a"
+  dependencies:
+    require-all "^2.2.0"
+    vue-eslint-parser "^2.0.1"
+
+eslint-scope@^3.7.1:
+  version "3.7.1"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
+  dependencies:
+    esrecurse "^4.1.0"
+    estraverse "^4.1.1"
+
+eslint-visitor-keys@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
+
 eslint@^3.10.1:
   version "3.19.0"
   resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc"
@@ -2444,6 +2466,13 @@ espree@^3.4.0:
     acorn "^5.1.1"
     acorn-jsx "^3.0.0"
 
+espree@^3.5.2:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca"
+  dependencies:
+    acorn "^5.2.1"
+    acorn-jsx "^3.0.0"
+
 esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1:
   version "2.7.3"
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
@@ -5546,6 +5575,10 @@ request@^2.81.0:
     tunnel-agent "^0.6.0"
     uuid "^3.0.0"
 
+require-all@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/require-all/-/require-all-2.2.0.tgz#b4420c233ac0282d0ff49b277fb880a8b5de0894"
+
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -6504,6 +6537,17 @@ void-elements@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
 
+vue-eslint-parser@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.1.tgz#30135771c4fad00fdbac4542a2d59f3b1d776834"
+  dependencies:
+    debug "^3.1.0"
+    eslint-scope "^3.7.1"
+    eslint-visitor-keys "^1.0.0"
+    espree "^3.5.2"
+    esquery "^1.0.0"
+    lodash "^4.17.4"
+
 vue-hot-reload-api@^2.2.0:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.2.4.tgz#683bd1d026c0d3b3c937d5875679e9a87ec6cd8f"
-- 
2.30.9