
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:
- Snippets de código JavaScript (ES6)
- Fragmentos ES7 React / Redux / GraphQL / React-Native
- Snippets de VSCode Vue
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:
- Crie uma conta no mercado (lembre-se do valor do campo do editor)
- Instale a biblioteca vsce globalmente
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.