Vetur Terminal Interface

Roppongi.vue #5

@ktsn

Core Team Member of Vue.js

目次

  • Vetur とは
  • テンプレート解析の現状
  • Vetur Terminal Interface (VTI)
  • VTI の実装
  • 予定中の機能追加・改善

Vetur とは

シンタックスハイライト

各ブロックの言語機能を提供

テンプレート解析の現状

テンプレート式の解析

フラグを ON にして使う

Vetur > Experimental: Template Interpolation Service

Vetur Terminal Interface
(VTI)

CLI から
template
型チェック

Demo

npm install -g vti
vti diagnostics

https://vuejs.github.io/vetur/vti.html

インストール

テンプレートの型チェック

VTI の実装

Template Transformer

<template>
  <div>{{ message }}</div>
</template>
<template>
  <div>{{ message }}</div>
</template>
{
  type: 'VElement',
  range: [ 13, 37 ],
  name: 'div',
  startTag: {
    type: 'VStartTag',
    range: [ 13, 18 ],
    selfClosing: false,
    attributes: []
  },
  children: [{
    type: 'VExpressionContainer',
    range: [ 18, 31 ],
    expression: {
      type: 'Identifier',
      range: [ 21, 28 ],
      name: 'message'
    }
  }],
  endTag: {
    type: 'VEndTag',
    range: [ 31, 37 ]
  }
}

vue-eslint-parser AST

{
  type: 'VElement',
  range: [ 13, 37 ],
  name: 'div',
  startTag: {
    type: 'VStartTag',
    range: [ 13, 18 ],
    selfClosing: false,
    attributes: []
  },
  children: [{
    type: 'VExpressionContainer',
    range: [ 18, 31 ],
    expression: {
      type: 'Identifier',
      range: [ 21, 28 ],
      name: 'message'
    }
  }],
  endTag: {
    type: 'VEndTag',
    range: [ 31, 37 ]
  }
}
<div>
{{ message }}
</div>
import __Component from "./Test.vue";
import { __vlsRenderHelper, __vlsComponentHelper, 
  __vlsIterationHelper } from "vue-editor-bridge";

__vlsRenderHelper(__Component, function () { 
  __vlsComponentHelper(this, "div", { 
    props: {}, 
    on: {}, 
    directives: [] 
  }, [
    this.message
  ]); 
});

{
  type: 'VElement',
  range: [ 13, 37 ],
  name: 'div',
  startTag: {
    type: 'VStartTag',
    range: [ 13, 18 ],
    selfClosing: false,
    attributes: []
  },
  children: [{
    type: 'VExpressionContainer',
    range: [ 18, 31 ],
    expression: {
      type: 'Identifier',
      range: [ 21, 28 ],
      name: 'message'
    }
  }],
  endTag: {
    type: 'VEndTag',
    range: [ 31, 37 ]
  }
}

TypeScript Code

{
  "pos": -1,
  "end": -1,
  "kind": 195,
  "expression": {
    "pos": -1,
    "end": -1,
    "kind": 75,
    "escapedText": "___vlsRenderHelper"
  },
  "arguments": [
    {
      "pos": -1,
      "end": -1,
      "kind": 75,
      "escapedText": "___Component"
    },
    {
      "pos": -1,
      "end": -1,
      "kind": 200,
      "parameters": [],
      "body": {
        "pos": -1,
        "end": -1,
        "kind": 222,
        "statements": []
      }
    }
  ]
 }

TypeScript AST

import __Component from "./Test.vue";
import { __vlsRenderHelper, __vlsComponentHelper, 
  __vlsIterationHelper } from "vue-editor-bridge";

__vlsRenderHelper(__Component, function () { 
  __vlsComponentHelper(this, "div", { 
    props: {}, 
    on: {}, 
    directives: [] 
  }, [
    this.message
  ]); 
});

<div>  </div>
{{ message }}
import __Component from "./Test.vue";
import { __vlsRenderHelper, __vlsComponentHelper, 
  __vlsIterationHelper } from "vue-editor-bridge";

__vlsRenderHelper(__Component, function () { 
  __vlsComponentHelper(this, "div", { 
    props: {}, 
    on: {}, 
    directives: [] 
  }, [
    this.message
  ]); 
});

import __Component from "./Test.vue";
import { __vlsRenderHelper, __vlsComponentHelper, 
  __vlsIterationHelper } from "vue-editor-bridge";

__vlsRenderHelper(__Component, function () { 
  __vlsComponentHelper(this, "div", { 
    props: {}, 
    on: {}, 
    directives: [] 
  }, [
    this.message
  ]); 
});

不正な Location でクラッシュ

{
  "pos": -1,
  "end": -1,
  "kind": 195,
  "expression": {
    "pos": -1,
    "end": -1,
    "kind": 75,
    "escapedText": "___vlsRenderHelper"
  },
  "arguments": [
    {
      "pos": -1,
      "end": -1,
      "kind": 75,
      "escapedText": "___Component"
    },
    {
      "pos": -1,
      "end": -1,
      "kind": 200,
      "parameters": [],
      "body": {
        "pos": -1,
        "end": -1,
        "kind": 222,
        "statements": []
      }
    }
  ]
 }
import __Component from "./Test.vue";
import { __vlsRenderHelper, __vlsComponentHelper, 
  __vlsIterationHelper } from "vue-editor-bridge";

__vlsRenderHelper(__Component, function () { 
  __vlsComponentHelper(this, "div", { 
    props: {}, 
    on: {}, 
    directives: [] 
  }, [
    this.message
  ]); 
});

<template>
  <div>{{ message }}</div>
</template>

message: 21–28

message: 256–268

Location ズレ問題

VTI の実装

Source Map Generation

{
  "pos": -1,
  "end": -1,
  "kind": 195,
  "expression": {
    "pos": -1,
    "end": -1,
    "kind": 75,
    "escapedText": "___vlsRenderHelper"
  },
  "arguments": [
    {
      "pos": -1,
      "end": -1,
      "kind": 75,
      "escapedText": "___Component"
    },
    {
      "pos": -1,
      "end": -1,
      "kind": 200,
      "parameters": [],
      "body": {
        "pos": -1,
        "end": -1,
        "kind": 222,
        "statements": []
      }
    }
  ]
 }
{
  type: 'VElement',
  range: [ 13, 37 ],
  name: 'div',
  startTag: {
    type: 'VStartTag',
    range: [ 13, 18 ],
    selfClosing: false,
    attributes: []
  },
  children: [{
    type: 'VExpressionContainer',
    range: [ 18, 31 ],
    expression: {
      type: 'Identifier',
      range: [ 21, 28 ],
      name: 'message'
    }
  }],
  endTag: {
    type: 'VEndTag',
    range: [ 31, 37 ]
  }
}

vue-eslint-parser AST

TypeScript AST

ts.setSourceMapRange(tsNode, {
  pos: 13,
  end: 37
});
import __Component from "./Test.vue";
import { __vlsRenderHelper, __vlsComponentHelper, 
  __vlsIterationHelper } from "vue-editor-bridge";

__vlsRenderHelper(__Component, function () { 
  __vlsComponentHelper(this, "div", { 
    props: {}, 
    on: {}, 
    directives: [] 
  }, [
    this.message
  ]); 
});

{
  "pos": -1,
  "end": -1,
  "kind": 195,
  "expression": {
    "pos": -1,
    "end": -1,
    "kind": 75,
    "escapedText": "___vlsRenderHelper"
  },
  "arguments": [
    {
      "pos": -1,
      "end": -1,
      "kind": 75,
      "escapedText": "___Component"
    },
    {
      "pos": -1,
      "end": -1,
      "kind": 200,
      "parameters": [],
      "body": {
        "pos": -1,
        "end": -1,
        "kind": 222,
        "statements": []
      }
    }
  ]
 }
{
  "pos": 139,
  "end": 308,
  "kind": 195,
  "expression": {
    "pos": 139,
    "end": 158,
    "kind": 75,
    "escapedText": "___vlsRenderHelper"
  },
  "arguments": [
    {
      "pos": 159,
      "end": 170,
      "kind": 75,
      "escapedText": "___Component"
    },
    {
      "pos": 171,
      "end": 307,
      "kind": 200,
      "parameters": [],
      "body": {
        "pos": 183,
        "end": 307,
        "kind": 222,
        "statements": []
      }
    }
  ]
 }

正しい Location を持つ AST

Location を持たない AST

{
  "pos": -1,
  "end": -1,
  "kind": 195,
  "expression": {
    "pos": -1,
    "end": -1,
    "kind": 75,
    "escapedText": "___vlsRenderHelper"
  },
  "arguments": [
    {
      "pos": -1,
      "end": -1,
      "kind": 75,
      "escapedText": "___Component"
    },
    {
      "pos": -1,
      "end": -1,
      "kind": 200,
      "parameters": [],
      "body": {
        "pos": -1,
        "end": -1,
        "kind": 222,
        "statements": []
      }
    }
  ]
 }
{
  "pos": 139,
  "end": 308,
  "kind": 195,
  "expression": {
    "pos": 139,
    "end": 158,
    "kind": 75,
    "escapedText": "___vlsRenderHelper"
  },
  "arguments": [
    {
      "pos": 159,
      "end": 170,
      "kind": 75,
      "escapedText": "___Component"
    },
    {
      "pos": 171,
      "end": 307,
      "kind": 200,
      "parameters": [],
      "body": {
        "pos": 183,
        "end": 307,
        "kind": 222,
        "statements": []
      }
    }
  ]
 }

元の Location を持つ AST

変換後の Location を持つ AST

Source Map

message: [21, 28] – [256, 268]

ts.getSourceMapRange(tsNode)

ただの Source Map では不十分

'Hello'+message
'Hello' + message

'Hello' + message:

[0, 15] – [0, 17]

Source Map

'Hello':

message:

[8, 15] – [10, 17]

[0, 7] – [0, 7]

'Hello'+message
'Hello' + message

'Hello' + message:

[0, 15] – [0, 17]

Source Map

'Hello':

message:

[0, 1, 2, 3, 4, 5, 6, 7]

– [0, 1, 2, 3, 4, 5, 6, 7]

[8, 9, 10, 11, 12, 13, 14, 15]

– [10, 11, 12, 13, 14, 15, 16, 17]

予定中の機能追加・改善

Props の型チェック

Template Transformer の改善

<template>
  <div v-if="post">
    <button @click="onClick(post)">Click</button>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import { Post } from './post'

export default Vue.extend({
  data() {
    return {
      post: null as Post | null
    }
  },
  
  methods: {
    onClick(post: Post): void { /* ... */}
  }
})
</script>

Post | null ✅

Post ✅

Post | null ❌

まとめ

  • VTI により CLI からテンプレート型チェックが可能に
  • テンプレート → TS 変換で型チェック
  • Source Map を用いてエラー位置をマッピング
  • Props の型チェック、チェックの精度向上を予定

Vetur Terminal Interface

By katashin

Vetur Terminal Interface

Roppongi.vue #5 https://roppongi-vue.connpass.com/event/164185/

  • 2,379