개발 일지 / / 2023. 8. 29. 06:41

고장난 vscode 확장을 수정하는 방법과 vue 플러그인 소개

 

개요 및 확장 플러그인 소개

 

최근 vue.js를 공부하면서 간단한 관리자 페이지를 만들고 있습니다.

사실 예전에 리액트도 잠깐 찍먹했었는데 진입장벽이 높더군요..

 

이번 게시물에서는 유용한 플러그인을 소개하고

작동이 안되는 플러그인을 어떻게 고쳤는지를 공유하겠습니다.

 

vue는 vscode로 하시는 분이 많을텐데요.

vscode의 vue 플러그인 중에는 vue 공식 문서에서도 권장되는 확장 플러그인이 있습니다.

https://marketplace.visualstudio.com/items?itemName=Vue.volar 

 

Vue Language Features (Volar) - Visual Studio Marketplace

Extension for Visual Studio Code - Language support for Vue 3

marketplace.visualstudio.com

volar 라는 이름의 플러그인입니다.

이것을 깔면 vue 파일인 .vue 확장자를 인식하고 vue 문법 자동 완성과 다양한 편집 기능을 사용할 수 있게 됩니다.

 

아래의 플러그인도 매우 유용하니 같이 설치하시길 강력 추천합니다.

https://marketplace.visualstudio.com/items?itemName=sdras.vue-vscode-snippets 

 

Vue VSCode Snippets - Visual Studio Marketplace

Extension for Visual Studio Code - Snippets that will supercharge your Vue workflow

marketplace.visualstudio.com

이 플러그인을 사용하면 vue를 사용하기 위한 여러 종류의 시작 템플릿을 짧은 약어로 자동 완성할 수 있고

심지어 vue 문법 관련 템플릿도 자동 완성 시켜주어 vue 클라이언트 개발이 매우 편리해집니다.

좌: 시작 템플릿 자동 완성 전 우: 자동 완성 후, 주석은 임의로 달았습니다.
좌: 문법 템플릿 자동 완성 전 우: 자동 완성 후 (vue의 요소 반복 문법)

위의 두 플러그인만 설치해도 필요한 기능은 대부분 들어가 있지만

편집 옵션이 다양하지 않고, 조금 아쉬운 기능도 있을 수 있습니다.

 

저에게 가장 필요한 기능은

프로젝트 내부의 컴포넌트 이름을 자동 완성과 동시에 임포트를 해주는 기능이었습니다.

리액트를 공부할 때도 정말 유용하다고 생각했는데

이게 없으면 import를 수동으로 해야되서 귀차니즘이 차곡차곡 쌓입니다..

volar에도 컴포넌트 자동 완성 기능이 있지만 항상 여는 태그에만 작동하며 태그가 닫히지 않습니다.

(이글을 쓰는 당시엔 volar의 컴포넌트 이름 자동 완성 및 자동 임포트가 작동하지 않았습니다.)

이런 아쉬운 점은 vscode의 장점인 플러그인을 추가로 설치해서 보완할 수 있지만

충돌이 일어날 수 있으므로 주의하셔야 합니다.

 

여는 태그가 없이도 컴포넌트 이름이 자동 완성되는 플러그인을 찾아보았습니다.

vue import 혹은 vue component라고 검색하니 관련된 여러 플러그인을 찾을 수 있었는데요.

대부분은 자바스크립트나 타입스크립트용이었고

이외엔 vue2 시기에 개발된 플러그인들이라 그런지 작동이 안되는 플러그인이 많았습니다.

 

결국 다음 두가지 플러그인을 남겨놨는데 하나씩 나사가 빠져있어서

자동 완성 목록이 표시만 되고 실제론 입출력이 안되거나 (vue-component)

자동 완성 시 주변 코드를 지워버리며 등장하는 엄청난 존재감을 자랑하기도 했습니다.. (vue-import)

 

이러한 상황에서 문제의 원인을 찾고 해결해보도록 하겠습니다.

 

 

플러그인 고치기

 

vscode 플러그인은 자바스크립트로 작성되며 플러그인 디렉토리에 접근하면 소스코드를 확인할 수 있습니다.

오픈소스는 이로운 점이 정말 많은 것 같습니다.

제 컴퓨터에서는 'C:/Users/{사용자명}/.vscode/extensions' 경로에 플러그인이 모여있었습니다.

 

조금 전에 얘기한 'vue-import'와 'vue-component' 두 플러그인 모두 제대로 작동하도록 하는데 성공했지만

이번 게시물에서는 최종적으로 사용하고 있는 vue-component를 고쳐보겠습니다.

이 플러그인의 문제는 자동 완성 목록은 표시되지만 텍스트가 입출력되지 않습니다.

vue-component 디렉토리에 진입한 다음 src 디렉토리로 이동합니다.

플러그인의 소스 파일들이 위치해 있습니다.

extensions.js가 메인 소스이고 이외 파일엔 개발자가 편의용으로 작성한 유틸 함수가 모여있습니다.

extention.js 파일을 열어보겠습니다.

더보기
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
const vscode = require('vscode')
const path = require('path')
const fs = require('fs')
const { parseFile, parseDocument } = require('./parse')
const {
  isInTag,
  getComNameByPosition,
  getRelativePath,
  toPascalCase,
  toKebabCase
} = require('./utils')
const {
  window,
  workspace,
  languages,
  commands,
  CompletionItemKind,
  CompletionItem,
  SnippetString,
  Uri,
  MarkdownString,
  Hover,
  Location,
  Position,
} = vscode

async function activate (context) {
  let allVueFilesSet
  let snipsnapList
  /**
  * 获取snipsnapList,为组件自动提示服务
  */
  function getSnipsnapList (filesSet) {
    return [...filesSet].map(file => {
      const name = path.basename(file, '.vue')
      const snipsnap = new CompletionItem(name, CompletionItemKind.Constructor)
      snipsnap.insertText = '' // 依靠后续命令,在确定组件后再解析文件、插入代码
      snipsnap.file = file // 添加一个自定义属性来记录
      snipsnap.command = { title: 'Import vue', command: 'vueComponent.importVue', arguments: [file, name] }
      return snipsnap
    })
  }
  // 获取所有vue文件并监听vue文件增删
  const exclude = workspace.getConfiguration().get('vueComponent.exclude')
  allVueFilesSet = new Set((await workspace.findFiles('**/*.vue', exclude)).map(f => f.fsPath))
  snipsnapList = getSnipsnapList(allVueFilesSet)
  const watcher = workspace.createFileSystemWatcher('**/*.vue')
  watcher.onDidCreate(e => {
    allVueFilesSet.add(e.fsPath)
    snipsnapList = getSnipsnapList(allVueFilesSet)
  })
  watcher.onDidDelete(e => {
    allVueFilesSet.delete(e.fsPath)
    snipsnapList = getSnipsnapList(allVueFilesSet)
  })

  /**
  * 组件自动提示
  */
  const componentsProvider = languages.registerCompletionItemProvider('vue', {
    async provideCompletionItems (document, position) {
      if (!isInTag(document, position, 'template') || getComNameByPosition(document, position)) return
      return snipsnapList.filter(snipsnap => {
        if (snipsnap.file === document.fileName) return false
        if (!snipsnap.detail) snipsnap.detail = getRelativePath(document.fileName, snipsnap.file)
        return true
      })
    },
  })

  /**
  * 跳转到组件文件
  */
  const linkProvider = languages.registerDefinitionProvider('vue', {
    provideDefinition (document, position) {
      if (!isInTag(document, position, 'template')) return
      const comName = toPascalCase(document.getText(document.getWordRangeAtPosition(position, '/[\w\-]+/')))
      let file = parseDocument(document).components[comName]
      if (file) {
        if (!fs.existsSync(file)) {
          file = path.join(vscode.workspace.rootPath, 'node_modules', file)
          if (!fs.existsSync(file)) return
        }
        return new Location(Uri.file(file), new Position(0, 0))
      }
    }
  })

  /**
  * 悬停提示
  */
  const hoverProvider = languages.registerHoverProvider('vue', {
    async provideHover (document, position) {
      if (!isInTag(document, position, 'template')) return
      const comName = getComNameByPosition(document, position)
      if (!comName) return
      const file = parseDocument(document).components[comName]
      const { props, events } = parseFile(file)
      const propsMdList = Object.keys(props).map(propName => {
        const prop = props[propName]
        let requiredText = ''
        let typeText = ''
        if (typeof prop === 'function') {
          typeText = `: ${prop.name}`
        } else if (Array.isArray(prop)) {
          typeText = `: ${prop.map(p => p.name).join()}`
        } else {
          const { required, type } = prop
          if (required) requiredText = '(required) '
          if (type) typeText = `: ${type.name}`
        }
        return new MarkdownString(`prop ${requiredText}${propName}${typeText}`)
      })
      const eventMdList = events.map(eventName => new MarkdownString(`event ${eventName}`))
      return new Hover(propsMdList.concat(eventMdList))
    }
  })

  /**
  * 属性事件自动提示
  */
  const propEventProvider = languages.registerCompletionItemProvider('vue', {
    provideCompletionItems (document, position) {
      if (!isInTag(document, position, 'template')) return
      const comName = getComNameByPosition(document, position)
      if (!comName) return
      const file = parseDocument(document).components[comName]
      const { props, events } = parseFile(file)
      const propsSnipsnap = Object.keys(props).map(prop => {
        const snipsnap = new CompletionItem(`p-${prop}`, CompletionItemKind.Property)
        snipsnap.insertText = new SnippetString(`:${toKebabCase(prop)}="$0"`)
        return snipsnap
      })
      const eventSnipsnap = events.map(event => {
        const snipsnap = new CompletionItem(`e-${event}`, CompletionItemKind.Event)
        snipsnap.insertText = new SnippetString(`@${event}="$0"`)
        return snipsnap
      })
      return propsSnipsnap.concat(eventSnipsnap)
    },
  })

  /**
  * 注册导入命令,出于性能考虑在确定补全的组件时才解析vue文件,仅内部使用
  */
  const importVue = commands.registerCommand('vueComponent.importVue', async (file, fileName) => {
    const editor = window.activeTextEditor
    const document = editor.document
    const fileNamePascal = toPascalCase(fileName)
    const tagName = workspace.getConfiguration('vueComponent').get('tagNameStyle') === 'kebab-case' ? toKebabCase(fileName) : fileNamePascal
    // 先在光标处插入组件代码
    const { props } = parseFile(file)
    let tabStop = 1
    const requiredPropsSnippetStr = Object.keys(props).filter(prop => props[prop].required)
      .reduce((accumulator, prop) => accumulator += ` :${toKebabCase(prop)}="$${tabStop++}"`, '')
    const snippetString = `<${tagName}${requiredPropsSnippetStr}>$0</${tagName}>`;
    await editor.insertSnippet(new SnippetString(snippetString))
    const components = parseDocument(document).components
    if (!components[fileNamePascal]) { // 没有注册组件,需要添加对应import、components
      const text = document.getText()
      // import代码      
      const scriptMatch = text.match(/\s+<script.*\s*/)
      let importPath = getRelativePath(document.fileName, file)
      if (importPath.includes('node_modules/')) {
        importPath = importPath.split('node_modules/')[1]
      }
      const importPosition = document.positionAt(scriptMatch.index + scriptMatch[0].length)
      const importStr = `import ${fileNamePascal} from '${importPath}'\n`
      // components代码
      let comPosition, comStr
      if (Object.keys(components).length > 0) { // 已有components属性
        const comMatch = text.match(/\s+components:\s{/)
        comPosition = document.positionAt(comMatch.index + comMatch[0].length)
        comStr = `\n    ${fileNamePascal},`
      } else {
        const comMatch = text.match(/\s+export default\s{/)
        comPosition = document.positionAt(comMatch.index + comMatch[0].length)
        comStr = `\n  components: { ${fileNamePascal} },`
      }

      editor.edit(edit => {
        edit.insert(importPosition, importStr);
        edit.insert(comPosition, comStr);
      })

    }
  });
  context.subscriptions.push(hoverProvider, componentsProvider, propEventProvider, linkProvider, importVue)
}
exports.activate = activate


module.exports = {
  activate
}

소스 코드와 개발자의 설명을 참고해보니

context.subscriptions.push(...) 메소드로 등록되는 각 기능은 다음과 같습니다.

(번역기를 돌려 일부 설명이 잘못됐을 수도 있습니다.)

 

componentsProvider: 작업 영역에서 컴포넌트를 탐색하고 태그와 함께 자동완성 기능을 제공합니다.

필요에 따라 props도 동시에 완성됩니다.

importVue: 자동 완성 사용 시 script 영역에 임포트 구문을 입출력 합니다.

 

hoverProvider: 컴포넌트 이름에 마우스를 가져다대면 props 및 이벤트를 표시합니다.

linkProvider: 컴포넌트 이름에 Ctrl + 좌클릭하면 해당 컴포넌트 파일로 이동합니다.

propEventProvider: 컴포넌트 태그 내부에서 'p'를 입력하면 속성 자동 완성,

'e' 이벤트를 입력하면 이벤트 자동 완성을 지원합니다.

 

다시 말하지만 이 플러그인의 주요 문제는 자동 완성 목록은 표시되지만 텍스트가 입출력되지 않는 것이었습니다.

 

오류를 찾는데는 디버깅만큼 효과적인 것이 없다고 생각하지만

플러그인을 디버깅할 수 있는지 검색해보아도 관련 정보를 찾을 수 없었고

자바스크립트에 대한 이해가 부족하여 코드만 보고 문제의 원인를 찾는 것도 어려운 상황이었습니다.

더군다나 거의 모든 플러그인의 소스 코드는 vscode API 문법을 사용하므로 저는 아래와 같이 생각했습니다.

 

디버깅도 안되고 문법도 잘 모르겠다.

=> 결국 하나씩 지워가며 테스트해보기로 했습니다.

 

테스트를 하기 위해선 자동 완성과 임포트 구문 입출력 기능만 있으면 될 것 같습니다.

우선 componentsProvider와 importVue를 제외한

hoverProvider, propEventProvider, linkProvider를 지워보겠습니다.

더보기
const vscode = require('vscode')
const path = require('path')
const fs = require('fs')
const { parseFile, parseDocument } = require('./parse')
const {
  isInTag,
  getComNameByPosition,
  getRelativePath,
  toPascalCase,
  toKebabCase
} = require('./utils')
const {
  window,
  workspace,
  languages,
  commands,
  CompletionItemKind,
  CompletionItem,
  SnippetString,
  Uri,
  MarkdownString,
  Hover,
  Location,
  Position,
} = vscode

async function activate (context) {
  let allVueFilesSet
  let snipsnapList

  function getSnipsnapList (filesSet) {
    return [...filesSet].map(file => {
      const name = path.basename(file, '.vue')
      const snipsnap = new CompletionItem(name, CompletionItemKind.Constructor)
      snipsnap.insertText = ''
      snipsnap.file = file
      snipsnap.command = { title: 'Import vue', command: 'vueComponent.importVue', arguments: [file, name] }
      return snipsnap
    })
  }

  const exclude = workspace.getConfiguration().get('vueComponent.exclude')
  allVueFilesSet = new Set((await workspace.findFiles('**/*.vue', exclude)).map(f => f.fsPath))
  snipsnapList = getSnipsnapList(allVueFilesSet)
  const watcher = workspace.createFileSystemWatcher('**/*.vue')
  watcher.onDidCreate(e => {
    allVueFilesSet.add(e.fsPath)
    snipsnapList = getSnipsnapList(allVueFilesSet)
  })
  watcher.onDidDelete(e => {
    allVueFilesSet.delete(e.fsPath)
    snipsnapList = getSnipsnapList(allVueFilesSet)
  })

  const componentsProvider = languages.registerCompletionItemProvider('vue', {
    async provideCompletionItems (document, position) {
      if (!isInTag(document, position, 'template') || getComNameByPosition(document, position)) return
      return snipsnapList.filter(snipsnap => {
        if (snipsnap.file === document.fileName) return false
        if (!snipsnap.detail) snipsnap.detail = getRelativePath(document.fileName, snipsnap.file)
        return true
      })
    },
  })

  const importVue = commands.registerCommand('vueComponent.importVue', async (file, fileName) => {
    const editor = window.activeTextEditor
    const document = editor.document
    const fileNamePascal = toPascalCase(fileName)
    const tagName = workspace.getConfiguration('vueComponent').get('tagNameStyle') === 'kebab-case' ? toKebabCase(fileName) : fileNamePascal
    const { props } = parseFile(file)
    let tabStop = 1
    const requiredPropsSnippetStr = Object.keys(props).filter(prop => props[prop].required)
      .reduce((accumulator, prop) => accumulator += ` :${toKebabCase(prop)}="$${tabStop++}"`, '')
    const snippetString = `<${tagName}${requiredPropsSnippetStr}>$0</${tagName}>`;
    await editor.insertSnippet(new SnippetString(snippetString))
    const components = parseDocument(document).components
    if (!components[fileNamePascal]) { // import、components
      const text = document.getText()
      // import代码      
      const scriptMatch = text.match(/\s+<script.*\s*/)
      let importPath = getRelativePath(document.fileName, file)
      if (importPath.includes('node_modules/')) {
        importPath = importPath.split('node_modules/')[1]
      }
      const importPosition = document.positionAt(scriptMatch.index + scriptMatch[0].length)
      const importStr = `import ${fileNamePascal} from '${importPath}'\n`
      // components代码
      let comPosition, comStr
      if (Object.keys(components).length > 0) {
        const comMatch = text.match(/\s+components:\s{/)
        comPosition = document.positionAt(comMatch.index + comMatch[0].length)
        comStr = `\n    ${fileNamePascal},`
      } else {
        const comMatch = text.match(/\s+export default\s{/)
        comPosition = document.positionAt(comMatch.index + comMatch[0].length)
        comStr = `\n  components: { ${fileNamePascal} },`
      }

      editor.edit(edit => {
        edit.insert(importPosition, importStr);
        edit.insert(comPosition, comStr);
      })

    }
  });
  context.subscriptions.push(componentsProvider, importVue)
}
exports.activate = activate


module.exports = {
  activate
}

테스트를 위한 최소 기능의 코드만 남겨두니 코드가 많이 짧아졌습니다.

 

vscode API를 참고해보면 editor의 edit(...) 메소드가 호출될 때

텍스트가 입력되어야 하지만 아직 입출력이 되지 않습니다.

edit(...)만 남겨놓고 실행해보면 텍스트 입력이 잘되므로 계속 진행해보겠습니다.

 

이제 구간별로 지워보며 탐색 범위를 줄여가면서 작동하는지 확인해보겠습니다.

(우리는 이걸 사이버 노가다라고 부르기로 했어요.)

다행인건 확인이 불필요한 코드는 최대한 줄이고 범위를 특정해서 탐색하므로 수고를 많이 덜었습니다.

 

edit.insert(comPosition, comStr) 코드와 관련된 코드를 보면 default export 등이 보이는데요.

vue3에선 Composition API라는 것이 도입되었고 사용을 적극 권장하기에

<script setup> 태그를 같이 사용함으로서 export와 return을 사용하지 않아도됩니다.

고로 관련된 코드를 지우겠습니다.

더보기
const vscode = require('vscode')
const path = require('path')
const fs = require('fs')
const { parseFile, parseDocument } = require('./parse')
const {
  isInTag,
  getComNameByPosition,
  getRelativePath,
  toPascalCase,
  toKebabCase
} = require('./utils')
const {
  window,
  workspace,
  languages,
  commands,
  CompletionItemKind,
  CompletionItem,
  SnippetString,
  Uri,
  MarkdownString,
  Hover,
  Location,
  Position,
} = vscode

async function activate (context) {
  let allVueFilesSet
  let snipsnapList

  function getSnipsnapList (filesSet) {
    return [...filesSet].map(file => {
      const name = path.basename(file, '.vue')
      const snipsnap = new CompletionItem(name, CompletionItemKind.Constructor)
      snipsnap.insertText = ''
      snipsnap.file = file
      snipsnap.command = { title: 'Import vue', command: 'vueComponent.importVue', arguments: [file, name] }
      return snipsnap
    })
  }

  const exclude = workspace.getConfiguration().get('vueComponent.exclude')
  allVueFilesSet = new Set((await workspace.findFiles('**/*.vue', exclude)).map(f => f.fsPath))
  snipsnapList = getSnipsnapList(allVueFilesSet)
  const watcher = workspace.createFileSystemWatcher('**/*.vue')
  watcher.onDidCreate(e => {
    allVueFilesSet.add(e.fsPath)
    snipsnapList = getSnipsnapList(allVueFilesSet)
  })
  watcher.onDidDelete(e => {
    allVueFilesSet.delete(e.fsPath)
    snipsnapList = getSnipsnapList(allVueFilesSet)
  })

  const componentsProvider = languages.registerCompletionItemProvider('vue', {
    async provideCompletionItems (document, position) {
      if (!isInTag(document, position, 'template') || getComNameByPosition(document, position)) return
      return snipsnapList.filter(snipsnap => {
        if (snipsnap.file === document.fileName) return false
        if (!snipsnap.detail) snipsnap.detail = getRelativePath(document.fileName, snipsnap.file)
        return true
      })
    },
  })

  const importVue = commands.registerCommand('vueComponent.importVue', async (file, fileName) => {
    const editor = window.activeTextEditor
    const document = editor.document
    const fileNamePascal = toPascalCase(fileName)
    const tagName = workspace.getConfiguration('vueComponent').get('tagNameStyle') === 'kebab-case' ? toKebabCase(fileName) : fileNamePascal
    const { props } = parseFile(file)
    let tabStop = 1
    const requiredPropsSnippetStr = Object.keys(props).filter(prop => props[prop].required)
      .reduce((accumulator, prop) => accumulator += ` :${toKebabCase(prop)}="$${tabStop++}"`, '')
    const snippetString = `<${tagName}${requiredPropsSnippetStr}>$0</${tagName}>`;
    await editor.insertSnippet(new SnippetString(snippetString))
    const components = parseDocument(document).components
    if (!components[fileNamePascal]) { // import、components
      const text = document.getText()
      // import代码      
      const scriptMatch = text.match(/\s+<script.*\s*/)
      let importPath = getRelativePath(document.fileName, file)
      if (importPath.includes('node_modules/')) {
        importPath = importPath.split('node_modules/')[1]
      }
      const importPosition = document.positionAt(scriptMatch.index + scriptMatch[0].length)
      const importStr = `import ${fileNamePascal} from '${importPath}'\n`
      editor.edit(edit => {
        edit.insert(importPosition, importStr);
      })
    }
  });
  context.subscriptions.push(componentsProvider, importVue)
}
exports.activate = activate


module.exports = {
  activate
}

그 후 edit(...) 메소드 위쪽 코드들을 부분적으로 지워가며 확인해보면서 몇가지의 원인을 찾아냈습니다.

우선 parseFile(file) 호출 후 requiredPropsSnippetStr를 초기화하는 과정에서

에러가 발생하여 메소드가 중단되는 것 같습니다.

requiredPropsSnippetStr는 컴포넌트의 props를 자동완성 시켜주는 기능인데

당장은 필요없으므로 선택된 부분을 지웠습니다.

 

그리고 조금 아래 코드를 보시면 components를 사용하는 부분이 있는데요.

이 부분도 문제가 되는 것 같아 parseDocument 메소드 내부로 들어가보았습니다.

parseDocument 메소드를 보면 parseResult에 componentsParse 객체를 저장하는 도중

예외가 발생하면 빈 객체가 저장됩니다.

빈 객체에서 components 속성에 접근하니 components에는 undefined가 저장되었고

if문에서 undefined[fileNamePascal]을 실행하면 예외가 발생해 중단될 수 밖에 없었습니다.

 

if문 내부의 코드를 보면 현재 파일이 아닌 다른 컴포넌트 파일을 호출하는 경우에

import 구문을 추가하는 내용입니다.

즉, 현재 파일 이름과 자동 완성하려는 컴포넌트 이름이 다른 경우에만 실행해야 합니다.

vscode.document는 현재 파일의 정보를 가지는 객체입니다.

 

다음과 같이 고치면 됩니다. 

 

이제 플러그인을 재설치한 다음 이번엔 다른 기능을 지울 필요 없이 조금 전 말한 것들만 수정하면

이제 자동 완성과 자동 import 모두 잘 작동합니다!

중복 임포트를 제한하는 등의 사소한 수정만 해주면 깔끔하게 사용할 수 있습니다.

혹시나 방법이 궁금하신 분은 말씀주시면 알려드리겠습니다.

사실 이 플러그인은 다른 기능도 제대로 작동하지 않는 것 같은데

예외 발생의 원인인 parse 등의 메소드들을 파일 형식에 맞게 수정하면

다른 기능들도 제대로 작동하지 않을까 싶습니다.

 

다음 오픈소스 기여는 volar에 기여할 수 있도록 확장 공부를 조금 더 해봐야겠습니다.

volar에 기여하면 엄청 뿌듯할 것 같네요.

업데이트: 현재는 리액트를 사용하며 ide와 플러그인 지원은 잘 작동합니다.

출처

Github Repository: 'vue-component'

 

GitHub - zbczbc2006/vscode-vue-component

Contribute to zbczbc2006/vscode-vue-component development by creating an account on GitHub.

github.com

VS code API

 

VS Code API

Visual Studio Code extensions (plug-in) API Reference.

code.visualstudio.com

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유