Snippet, uma extensão para VSCode e CLI. Parte 1





Bom dia amigos!



Enquanto desenvolvia o Modern HTML Starter Template, pensei em expandir sua usabilidade. Naquela época, as opções de uso se limitavam à clonagem do repositório e ao download do arquivo. É assim que o trecho de HTML e a extensão para Microsoft Visual Studio Code - HTML Template , bem como a interface de linha de comando - create-modern-template apareceram . É claro que essas ferramentas estão longe de ser perfeitas e irei refiná-las o máximo que puder. No entanto, no processo de criação deles, aprendi algumas coisas interessantes que quero compartilhar com você.



Abordaremos o snippet e a extensão nesta parte e a CLI na próxima.



Se você está interessado apenas no código-fonte, aqui está o link para o repositório .



Trecho



O que é um snippet? Resumindo, um snippet é um modelo que o editor usa para autocomplete (autocompletar).



O VSCode foi construído em Emmet ( site oficial , Emmet no Visual Studio Code ), que usa vários snippets HTML, CSS e JS para ajudá-lo a escrever seu código. Digitamos no editor (em .html) !, Pressione Tab ou Enter, obtemos a marcação HTML5 concluída. Nós digitamos nav> ul> li * 3> a.link> img, pressionamos Tab, obtemos:



<nav>
    <ul>
      <li><a href="" class="link"><img src="" alt=""></a></li>
      <li><a href="" class="link"><img src="" alt=""></a></li>
      <li><a href="" class="link"><img src="" alt=""></a></li>
    </ul>
  </nav>

      
      





etc.



Além dos integrados, o VSCode oferece a capacidade de usar snippets personalizados. Para criá-los, vá para Arquivo -> Preferências -> Trechos do usuário (ou clique no botão Gerenciar no canto inferior esquerdo e selecione Trechos do usuário). As configurações de cada idioma são armazenadas em um arquivo JSON correspondente (para HTML em html.json, para JavaScript em javascript.json, etc.).



Vamos praticar a criação de snippets JS. Encontre o arquivo javascript.json e abra-o.







Vemos comentários que descrevem resumidamente as regras para a criação de snippets. Mais informações sobre a criação de snippets personalizados no VSCode podem ser encontradas aqui .



Vamos começar com algo simples. Vamos criar um snippet para console.log (). Isto é o que parece:



"Print to console": {
  "prefix": "log",
  "body": "console.log($0)",
  "description": "Create console.log()"
},

      
      





  • Imprimir no console - chave do objeto, nome do snippet (obrigatório)
  • prefixo - abreviação de snippet (obrigatório)
  • body - o próprio snippet (obrigatório)
  • $ number - posição do cursor após a criação do snippet; $ 1 - primeira posição, $ 2 - segundo, etc., $ 0 - última posição (opcional)
  • descrição - descrição do snippet (opcional)


Salvamos o arquivo. Digitamos log no script, pressionamos Tab ou Enter, obtemos console.log () com o cursor entre os colchetes.



Vamos criar um snippet para o loop for-of:



"For-of loop": {
  "prefix": "fo",
  "body": [
    "for (const ${1:item} of ${2:arr}) {",
    "\t$0",
    "}"
  ]
},

      
      





  • Snippets de várias linhas são criados usando uma matriz
  • $ {número: valor}; $ {1: item} significa a primeira posição do cursor com o valor padrão do item; este valor é destacado após a criação de um snippet, bem como após mover para a próxima posição do cursor para edição rápida
  • \ t - um recuo (a quantidade de espaço é determinada pelas configurações correspondentes do editor ou, no meu caso, a extensão mais bonita ), \ t \ t - dois recuos, etc.


Nós digitamos fo no script, pressionamos Tab ou Enter, obtemos:



for (const item of arr) {

}

      
      





com o item destacado. Pressione Tab, arr é realçado. Pressione Tab novamente e vá para a segunda linha.



Aqui estão mais alguns exemplos:



"For-in loop": {
  "prefix": "fi",
  "body": [
    "for (const ${1:key} in ${2:obj}) {",
    "\t$0",
    "}"
  ]
},
"Get one element": {
  "prefix": "qs",
  "body": "const $1 = ${2:document}.querySelector('$0')"
},
"Get all elements": {
  "prefix": "qsa",
  "body": "const $1 = [...${2:document}.querySelectorAll('$0')]"
},
"Add listener": {
  "prefix": "al",
  "body": [
    "${1:document}.addEventListener('${2:click}', (${3:{ target }}) => {",
    "\t$0",
    "})"
  ]
},
"Async function": {
  "prefix": "af",
  "body": [
    "const $1 = async ($2) => {",
    "\ttry {",
    "\t\tconst response = await fetch($3)",
    "\t\tconst data = await res.json()",
    "\t\t$0",
    "\t} catch (err) {",
    "\t\tconsole.error(err)",
    "\t}",
    "}"
  ]
}

      
      





Os snippets de HTML seguem o mesmo princípio. Esta é a aparência do modelo HTML:



{
  "HTML Template": {
    "prefix": "html",
    "body": [
      "<!DOCTYPE html>",
      "<html",
      "\tlang='en'",
      "\tdir='ltr'",
      "\titemscope",
      "\titemtype='https://schema.org/WebPage'",
      "\tprefix='og: http://ogp.me/ns#'",
      ">",
      "\t<head>",
      "\t\t<meta charset='UTF-8' />",
      "\t\t<meta name='viewport' content='width=device-width, initial-scale=1' />",
      "",
      "\t\t<title>$1</title>",
      "",
      "\t\t<meta name='referrer' content='origin' />",
      "\t\t<link rel='canonical' href='$0' />",
      "\t\t<link rel='icon' type='image/png' href='./icons/64x64.png' />",
      "\t\t<link rel='manifest' href='./manifest.json' />",
      "",
      "\t\t<!-- Security -->",
      "\t\t<meta http-equiv='X-Content-Type-Options' content='nosniff' />",
      "\t\t<meta http-equiv='X-XSS-Protection' content='1; mode=block' />",
      "",
      "\t\t<meta name='author' content='$3' />",
      "\t\t<meta name='description' content='$2' />",
      "\t\t<meta name='keywords' content='$4' />",
      "",
      "\t\t<meta itemprop='name' content='$1' />",
      "\t\t<meta itemprop='description' content='$2' />",
      "\t\t<meta itemprop='image' content='./icons/128x128.png' />",
      "",
      "\t\t<!-- Microsoft -->",
      "\t\t<meta http-equiv='x-ua-compatible' content='ie=edge' />",
      "\t\t<meta name='application-name' content='$1' />",
      "\t\t<meta name='msapplication-tooltip' content='$2' />",
      "\t\t<meta name='msapplication-starturl' content='/' />",
      "\t\t<meta name='msapplication-config' content='browserconfig.xml' />",
      "",
      "\t\t<!-- Facebook -->",
      "\t\t<meta property='og:type' content='website' />",
      "\t\t<meta property='og:url' content='$0' />",
      "\t\t<meta property='og:title' content='$1' />",
      "\t\t<meta property='og:image' content='./icons/256x256.png' />",
      "\t\t<meta property='og:site_name' content='$1' />",
      "\t\t<meta property='og:description' content='$2' />",
      "\t\t<meta property='og:locale' content='en_US' />",
      "",
      "\t\t<!-- Twitter -->",
      "\t\t<meta name='twitter:title' content='$1' />",
      "\t\t<meta name='twitter:description' content='$2' />",
      "\t\t<meta name='twitter:url' content='$0' />",
      "\t\t<meta name='twitter:image' content='./icons/128x128.png' />",
      "",
      "\t\t<!-- IOS -->",
      "\t\t<meta name='apple-mobile-web-app-title' content='$1' />",
      "\t\t<meta name='apple-mobile-web-app-capable' content='yes' />",
      "\t\t<meta name='apple-mobile-web-app-status-bar-style' content='#222' />",
      "\t\t<link rel='apple-touch-icon' href='./icons/256x256.png' />",
      "",
      "\t\t<!-- Android -->",
      "\t\t<meta name='theme-color' content='#eee' />",
      "\t\t<meta name='mobile-web-app-capable' content='yes' />",
      "",
      "\t\t<!-- Google Verification Tag -->",
      "",
      "\t\t<!-- Global site tag (gtag.js) - Google Analytics -->",
      "",
      "\t\t<!-- Global site tag (gtag.js) - Google Analytics -->",
      "",
      "\t\t<!-- Yandex Verification Tag -->",
      "",
      "\t\t<!-- Yandex.Metrika counter -->",
      "",
      "\t\t<!-- Mail Verification Tag -->",
      "",
      "\t\t<!-- JSON-LD -->",
      "\t\t<script type='application/ld+json'>",
      "\t\t\t{",
      "\t\t\t\t'@context': 'http://schema.org/',",
      "\t\t\t\t'@type': 'WebPage',",
      "\t\t\t\t'name': '$1',",
      "\t\t\t\t'image': [",
      "\t\t\t\t\t'$0icons/512x512.png'",
      "\t\t\t\t],",
      "\t\t\t\t'author': {",
      "\t\t\t\t\t'@type': 'Person',",
      "\t\t\t\t\t'name': '$3'",
      "\t\t\t\t},",
      "\t\t\t\t'datePublished': '2020-11-20',",
      "\t\t\t\t'description': '$2',",
      "\t\t\t\t'keywords': '$4'",
      "\t\t\t}",
      "\t\t</script>",
      "",
      "\t\t<!-- Google Fonts -->",
      "",
      "\t\t<style>",
      "\t\t\t/* Critical CSS */",
      "\t\t</style>",
      "",
      "\t\t<link rel='preload' href='./css/style.css' as='style'>",
      "\t\t<link rel='stylesheet' href='./css/style.css' />",
      "",
      "<link rel='preload' href='./script.js' as='script'>",
      "\t</head>",
      "\t<body>",
      "\t\t<!-- HTML5 -->",
      "\t\t<header>",
      "\t\t\t<h1>$1</h1>",
      "\t\t\t<nav>",
      "\t\t\t\t<a href='#' target='_blank' rel='noopener'>Link 1</a>",
      "\t\t\t\t<a href='#' target='_blank' rel='noopener'>Link 2</a>",
      "\t\t\t</nav>",
      "\t\t</header>",
      "",
      "\t\t<main></main>",
      "",
      "\t\t<footer>",
      "\t\t\t<p>© 2020. All rights reserved</p>",
      "\t\t</footer>",
      "",
      "\t\t<script src='./script.js' type='module'></script>",
      "\t</body>",
      "</html>"
    ],
    "description": "Create Modern HTML Template"
  }
}

      
      





Nós digitamos html, pressionamos Tab ou Enter, obtemos a marcação. As posições do cursor são definidas na seguinte ordem: nome do aplicativo (título), descrição (descrição), autor (autor), palavras-chave (palavras-chave), endereço (url).



Extensão



O site VSCode possui uma documentação excelente sobre a construção de extensões .



Vamos criar duas opções para a extensão: formulário de snippet e formulário CLI. Publicaremos a segunda opção no Visual Studio Marketplace .



Exemplos de extensões na forma de snippets:





As extensões de formulário CLI são menos populares, provavelmente porque existem CLIs "reais".



Extensão na forma de snippets


Para desenvolver extensões para VSCode, além de Node.js e Git , precisamos de mais algumas bibliotecas, mais precisamente, uma biblioteca e um plugin, ou seja, yeoman e código-gerador . Instale-os globalmente:



npm i -g yo generator-code
// 
yarn global add yo generator-code

      
      





Executamos o comando yo code, selecionamos New Code Snippets e respondemos às perguntas.







Resta copiar o trecho de HTML que criamos anteriormente no arquivo snippets / snippets.code-snippets (os arquivos de trecho também podem ter a extensão json), editar o package.json e README.md e você pode publicar a extensão no mercado. Como você pode ver, tudo é muito simples. Muito simples, pensei, e decidi criar uma extensão na forma de uma CLI.



Extensão CLI


Execute o comando do código yo novamente. Desta vez selecionamos New Extension (TypeScript) (não tenha medo, não haverá quase nenhum TypeScript em nosso código e onde estiver darei as explicações necessárias), responda as perguntas.







Para ter certeza de que a extensão está funcionando, abra o projeto no editor:



cd htmltemplate
code .

      
      





Pressione F5 ou o botão Executar (Ctrl / Cmd + Shift + D) à esquerda e o botão Iniciar Depuração na parte superior. Às vezes, você obtém um erro na inicialização. Neste caso, cancele o lançamento (Cancelar) e repita o procedimento.



No editor que é aberto, clique em Exibir -> Paleta de comandos (Ctrl / Cmd + Shift + P), digite hello e selecione Hello World.







Recebemos uma mensagem informativa do VSCode e uma mensagem correspondente (parabéns) no console.







De todos os arquivos do projeto, estamos interessados ​​em package.json e src / extension.ts. O diretório src / test e o arquivo vsc-extension-quickstart.md podem ser removidos.



Vamos dar uma olhada em extension.ts (comentários removidos para facilitar a leitura):



//   VSCode
import * as vscode from 'vscode'

// ,    
export function activate(context: vscode.ExtensionContext) {
  // ,    ,
  //     
  console.log('Congratulations, your extension "htmltemplate" is now active!')

  //  
  //  -   
  // htmltemplate -  
  // helloWorld -  
  let disposable = vscode.commands.registerCommand(
    'htmltemplate.helloWorld',
    () => {
      //  ,   
      //    
      vscode.window.showInformationMessage('Hello World from htmltemplate!')
    }
  )

  //  
  //   ,     "/",
  //     ""
  context.subscriptions.push(disposable)
}

// ,    
export function deactivate() {}

      
      





Ponto importante: 'extension.command' em extension.ts deve corresponder aos valores dos activationEvents e dos campos de comando em package.json:



"activationEvents": [
  "onCommand:htmltemplate.helloWorld"
],
"contributes": {
  "commands": [
    {
      "command": "htmltemplate.helloWorld",
      "title": "Hello World"
    }
  ]
},

      
      





  • comandos - lista de comandos
  • activationEvents - funções a serem chamadas durante a execução do comando


Vamos começar a desenvolver a extensão.



Queremos que nossa extensão se pareça com criar-reagir-app ou vue-cli na funcionalidade , ou seja, no comando create criou um projeto contendo todos os arquivos necessários no diretório de destino.



Primeiro, vamos editar o package.json:



"displayName": "HTML Template",
"activationEvents": [
  "onCommand:htmltemplate.create"
],
"contributes": {
  "commands": [
    {
      "command": "htmltemplate.create",
      "title": "Create Template"
    }
  ]
},

      
      





Crie um diretório src / components para armazenar arquivos de projeto que serão copiados para o diretório de destino.



Criamos arquivos de projeto na forma de módulos ES6 (VSCode usa módulos ES6 por padrão (exportar / importar), mas suporta módulos CommonJS (module.exports / require)): index.html.js, css / style.css.js , script.js, etc. O conteúdo dos arquivos é exportado por padrão:



// index.html.js
export default `
<!DOCTYPE html>
<html
  lang="en"
  dir="ltr"
  itemscope
  itemtype="https://schema.org/WebPage"
  prefix="og: http://ogp.me/ns#"
>
  ...
</html>
`

      
      





Observe que, com essa abordagem, todas as imagens (em nosso caso, ícones) devem ser codificadas em Base64: aqui está uma ferramenta online adequada . A presença da linha "data: image / png; base64," no início do arquivo convertido não é de fundamental importância.



Usaremos fs-extra para copiar (gravar) arquivos . O método outputFile desta biblioteca faz a mesma coisa que o método writeFile embutido do Node.js, mas também cria um diretório para o arquivo que está sendo gravado, se ele não existir: por exemplo, se especificarmos create css / style.css e o diretório css não existir, outputFile o criará e escreverá style.css lá (writeFile lançará uma exceção se não houver diretório).



O arquivo extension.ts tem esta aparência:



import * as vscode from 'vscode'
//   fs-extra
const fs = require('fs-extra')
const path = require('path')

//   , ,   
import indexHTML from './components/index.html.js'
import styleCSS from './components/css/style.css.js'
import scriptJS from './components/script.js'
import icon64 from './components/icons/icon64.js'
// ...

export function activate(context: vscode.ExtensionContext) {
  console.log('Congratulations, your extension "htmltemplate" is now active!')

  let disposable = vscode.commands.registerCommand(
    'htmltemplate.create',
    () => {
      //  ,       html-template
      // filename: string  TypeScript-,
      //   ,  ,
      //   
      const folder = (filename: string) =>
        path.join(vscode.workspace.rootPath, `html-template/${filename}`)

      //    
      // files: string[] ,    files   
      const files: string[] = [
        indexHTML,
        styleCSS,
        scriptJS,
        icon64,
        ...
      ]

      //    
      //  ,        
      const fileNames: string[] = [
        'index.html',
        'css/style.css',
        'script.js',
        'server.js',
        'icons/64x64.png',
        ...
      ]

      ;(async () => {
        try {
          //    
          for (let i = 0; i < files.length; i++) {

            //  outputFile       :
            //    ( ),     (  UTF-8)

            //     png,
            // ,     Base64-:
            //   
            if (fileNames[i].includes('png')) {
              await fs.outputFile(folder(fileNames[i]), files[i], 'base64')
            // ,    
            } else {
              await fs.outputFile(folder(fileNames[i]), files[i])
            }
          }

          //     
          return vscode.window.showInformationMessage(
            'All files created successfully'
          )
        } catch {
          //   
          return vscode.window.showErrorMessage('Failed to create files')
        }
      })()
    }
  )

  context.subscriptions.push(disposable)
}

export function deactivate() {}

      
      





Para evitar que o TypeScript preste atenção à falta de tipos de arquivos de módulo importados, crie src / global.d.ts com o seguinte conteúdo:



declare module '*'

      
      





Vamos testar a extensão. Abra-o no editor:



cd htmltemplate
code .

      
      





Comece a depuração (F5). Vá para o diretório de destino (test-dir, por exemplo) e execute o comando create na Paleta de Comandos.







Recebemos uma mensagem informativa sobre o sucesso da criação de arquivos. Hooray!







Publicação de uma extensão para o Visual Studio Marketplace


Para poder publicar extensões para VSCode, você precisa fazer o seguinte:





Editando package.json:



{
  "name": "htmltemplate",
  "displayName": "HTML Template",
  "description": "Modern HTML Starter Template",
  "version": "1.0.0",
  "publisher": "puslisher-name",
  "license": "MIT",
  "keywords": [
    "html",
    "html5",
    "css",
    "css3",
    "javascript",
    "js"
  ],
  "icon": "build/128x128.png",
  "author": {
    "name": "Author Name @githubusername"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/username/dirname"
  },
  "engines": {
    "vscode": "^1.51.0"
  },
  "categories": [
    "Snippets"
  ],
  "activationEvents": [
    "onCommand:htmltemplate.create"
  ],
  "main": "./dist/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "htmltemplate.create",
        "title": "Create Template"
      }
    ]
  },
  ...
}

      
      





Editando README.md.



Execute o comando vsce package no diretório de extensão para criar um pacote publicado com a extensão vsix. Obtemos o arquivo htmltemplate-1.0.0.vsix.



Na página de gerenciamento de extensões de mercado, clique no botão Nova extensão e selecione Código do Visual Studio. Transfira ou carregue o arquivo VSIX na janela modal. Estamos aguardando a conclusão da verificação.







Depois que uma marca de seleção verde aparece ao lado do número da versão, a extensão se torna disponível para instalação no VSCode.







Para atualizar a extensão, você precisa alterar o número da versão em package.json, gerar um arquivo VSIX e carregá-lo no marketplace clicando no botão Mais ações e selecionando Atualizar.



Como você pode ver, não há nada de sobrenatural na criação e publicação de extensões para VSCode. Sobre isso, deixe-me sair.



Na próxima parte, criaremos uma interface de linha de comando completa, primeiro usando o framework Heroku - oclif , depois sem ele. Nosso Node.js-CLI será muito diferente da extensão, ele terá alguma visualização, a capacidade de inicializar git opcionalmente e instalar dependências.



Espero que você tenha encontrado algo interessante para você. Obrigado pela atenção.



All Articles