Dan Mindru
Dan Mindru

Reputation: 6104

How to get Flow to properly work with Vue 2 (webpack)?

I'm trying to add Flow to the Vue 2 webpack-template. For the record, I'm on runtime-only (files follow the .vue format / standard).

My first attempt was to use flow through the cli, which I realized it's not going to work because it didn't know how to handle .vue files.

My second attempt was to add a webpack loader (namely flow-status-webpack-plugin) and run Flow check as part of the build (like eslint works for example). That didn't work out, so I looked into other options.

My third attempt was to use a babel plugin, which was fairly successful at first. I used babel-plugin-typecheck + babel-plugin-syntax-flow. There's no output in Webpack, however a type error would break the app. I'm fine with this approach; it'll work fine with a CI and break the build.

Here's how my .babelrc looked:

{
  ...
  "plugins": [
    ...
    ["typecheck", {
      "disable": {
        "production": true
      }
    }],
    "syntax-flow",
    "transform-flow-strip-types"
  ],
  ...
}

At this point, Flow works as expected for global methods, but doesn't work inside a Vue component:

<template>...</template>

<script>
/* @flow */
const flowIt = (a: number): number => {
  return a * 10
}

flowIt(20)
flowIt('bah') // Uncaught TypeError: Value of argument "a" violates contract. Expected: number Got: string

export default {    
  mounted: function () {
    flowIt(20)
    flowIt('bah') // Sees nothing wrong here
  }
}
</script>

<style>...</style>

On top of that, the goal is to not change the app code because of Flow. Ideally, I'd just use Vue as normally:

<template>...</template>

<script>
/* @flow */
export default {  
  methods: {
    flowIt (a: number): number {
      return a * 10
    }
  },

  mounted: function () {
    this.flowIt(20)
    this.flowIt('bah') // Should throw a type error.
  }
}
</script>

<style>...</style>

Not sure if this has that much to do with Vue as it has with my experience with Flow (hint: not that experienced). I'm thinking I need some type files that make Flow 'understand' how a Vue component is structured (same for directives I guess).

To those that have more experience with it, how did you get Flow to properly work with Vue + webpack?

Upvotes: 18

Views: 8281

Answers (5)

sobolevn
sobolevn

Reputation: 18080

I have implemented a project template for vue with flow. https://github.com/wemake-services/wemake-vue-template It supports single file components, linting, tests with jest, building, and server side rendering.

vue

Here's what your components would look like:

<template>
...
</template>

<script>
// @flow

import Vue from 'vue'
import { Store } from 'vuex'
import Component from 'nuxt-class-component'
import { Getter, State } from 'vuex-class'
import AppLogo from '~/components/AppLogo'
import Comment from '~/components/Comment'
import type { CommentType, StateType } from '~/types'

@Component({
  components: {
    AppLogo,
    Comment
  }
})
export default class Index extends Vue {
  @State('comments') comments: Array<CommentType>
  @Getter('hasComments') hasComments: boolean

  fetch (
    { store, app }: { store: Store<StateType>, app: Vue }
  ): Promise<Array<CommentType>> {
    // Uncomment the next line to test flow types:
    // console.log(this.comments + 12)
    return store.dispatch('fetchComments', app)
  }
}
</script>

It requires several things to configure:

  1. Dependencies. Everything with flow in its name from here: https://github.com/wemake-services/wemake-vue-template/blob/master/template/package.json
  2. Creating valid .flowconfig. This might be tricky: https://github.com/wemake-services/wemake-vue-template/blob/master/template/.flowconfig
  3. Configuring .babelrc: https://github.com/wemake-services/wemake-vue-template/blob/master/template/.babelrc
  4. Configuring eslint: https://github.com/wemake-services/wemake-vue-template/blob/master/template/.eslintrc
  5. Triggering flow-typed install after each normal installation: https://github.com/wemake-services/wemake-vue-template/blob/master/template/package.json#L12

vuex

You are also able to annotate vuex store: state, commit handlers, getters, and actions. You can use @vue-flow-typed/vuex for this part.

That's how it looks:

type StateType = {
  comments: string[]
}

function state (): StateType {
  return {
    comments: null
  }
}

const getters = {
  hasComments (state: StateType): boolean {
    return Boolean(state.comments && state.comments.length > 0)
  }
}

const mutations = {
  'SET_COMMENTS': (
    state: StateType, comments: string[]
  ) => {
    state.comments = comments
  }
}

const actions = {
  async fetchComments (
    { commit, state }: ActionContext<StateType>
  ) {
    const data = await Promise.resolve(['good', 'nice'])
    commit('SET_COMMENTS', data)
    // Uncomment next line to see typing in action:
    // console.log(state.comments, state.fake)

    return data
  }
}

But beware, that it is still impossible to annotate some parts. Read more about known issues here: https://github.com/sobolevn/vue-flow-typed#known-problems

Upvotes: 1

Dan Mindru
Dan Mindru

Reputation: 6104

Using eslint + flow

This is yet another approach to integrating flow & vue. Meanwhile, flow came to eslint. Therefore, we can get flow errors straight as lint errors. It's a cleaner approach, but then flow becomes coupled with your build process (you can't run flow check independently, but need to run your entire build pipeline via webpack to get the errors). Still waiting for this issue to be resolved to have full flow support in .vue files as of 10th of May 2017.

For most cases that's fine, but some might still want the flexibility (and speed) of running flow check. That might also depend on your CI setup.

Here's how you can setup flow and eslint:

  1. Install deps

    yarn add \
      babel-plugin-syntax-flow \
      babel-plugin-transform-class-properties \
      babel-plugin-transform-flow-strip-types \
      eslint \
      babel-eslint \
      eslint-plugin-html \
      eslint-plugin-flowtype-errors \
      eslint-plugin-vue \
      eslint-config-vue \
      flow-bin \
    -D
    
  2. Configure .babelrc

    {
      ...
      "plugins": [
        "babel-plugin-transform-class-properties",
        "babel-plugin-syntax-flow",
        "babel-plugin-transform-flow-strip-types"
      ]
    }
    
  3. Configure .eslintrc

    {
      "parser": "babel-eslint",
    
      "plugins": [
        "html",
        "flowtype-errors"
      ],
    
      "extends": [
        "vue"
      ],
    
      "rules": {
        "flowtype-errors/show-errors": 2
      }
    }
    
  4. Create a .flowconfig file. It can be empty if you have nothing to configure.

No other workarounds required in this case, you can then just use /* @flow */ in the script tags in any of your .vue files. See the original post here.

Upvotes: 6

musicformellons
musicformellons

Reputation: 13393

I think this has been solved in the meantime and now you can use Flow with Vue-components without hacks. See this rather brilliant article for config details: https://alligator.io/vuejs/components-flow/

Upvotes: 2

Dan Mindru
Dan Mindru

Reputation: 6104

In addition to Nik's answer, it's worth mentioning that combining his 'comment' strategy with a runtime checker makes 'the package' a tad more complete. One way to do it is by using babel-plugin-tcomb. That'll make the runtime checker part of the webpack / build process (on save) + flow check as part of a CI script.

For development, tcomb will do a runtime check and will throw an exception (console). It doesn't do static checks, so the following

<script>
/* @flow */
const flowIt = (a: number): number => {
  return '' // Sees nothing wrong here, should be a number
}

// Vue component
export default {    
  ...
}
</script>

won't work as expected. However, the following will:

<template>{{ foo('bar') }} <!-- Type error --></template>
<script>
/* @flow */
const flowIt = (a: number): number => {
  return '' // Type error
}

// Vue component
export default {    
  methods: {
    foo: (x) => { flowIt(x) // Type error }
  },

  mounted: () => {
    flowIt([]) // Type error
  }
}
</script>

That's not ideal, but it does check after each save and that'll catch most type errors. Worth mentioning: tcomb uses the same annotations (uses Flow internally), so it works out of the box.

Ofc, that's not good enough & kinda defeats the point of Flow. The solution to that is to still run flow check on the CI, as mentioned. That requires a number of changes:

  1. Update .flowconfig to load .vue files:

    ...
    [options]
    module.file_ext=.vue
    module.file_ext=.js
    ...
    
  2. Include the template & style block in the comment containing the @flow pragma; comment out the script tags (this approach was mentioned here):

    /* @flow
    <template>...</template>
    
    <style>...</style>
    */
    
    // <script>
    ...
    // </script>
    

    That's a bit awkward, but I could not find a better way. Ideally, Flow would be able to process <script> tags in a HTML doc, but that's just on the wishlist for now (see issue).

  3. Disable tcomb in production

    {
      ...
      "plugins": [
        ...
        "syntax-flow",
        "transform-flow-strip-types"
      ],
      "env": {
        "development": {
          "plugins": ["tcomb"]
        }
      }
    }
    

Upvotes: 4

Nik
Nik

Reputation: 1363

You can still use Flow for the JS portion of a .vue component, by commenting out the <template>, <style> and <script> portions:

 /* @flow
 <style>
 ...style definitions here
 </style>
 <template>
 ...html...
 </template>
 */
 // <script>
 export default {  
   methods: {
      flowIt (a: number): number {
         return a * 10
      }
   },

   mounted: function () {
      this.flowIt(20)
      this.flowIt('bah') //Won't throw error, as flowIt is attached to
                         //this.
   }
}
// </script>

The vue compiler will still recognize the <template>, <style> and <script> sections even when commented, but the Flow type checker will ignore them and only process the proper javascript section.

Unfortunately, this won't get you 100% type coverage, as Flow will not be able to check functions and objects attached to this (the Vue component itself), however, you can still benefit from Flow's type checking of calls out to external functions (e.g. Vuex actions and getters, other javascript imported modules), and if you have extended business logic within the component's methods, you can get some type safety when working with the method parameters.

Upvotes: 5

Related Questions