Сниппет, расширение для VSCode и CLI. Часть 1

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.


Доброго времени суток, друзья!

В процессе разработки Современного стартового HTML-шаблона я задумался о расширении возможностей его использования. На тот момент варианты его применения ограничивались клонированием репозитория и скачиванием архива. Так появились HTML-сниппет и расширение для Microsoft Visual Studio Code — HTML Template, а также интерфейс командной строки — create-modern-template. Конечно, указанные инструменты далеки от совершенства и я буду их дорабатывать по мере сил и возможностей. Однако, в процессе их создания я узнал несколько интересных вещей, которыми и хочу с вами поделиться.

В этой части мы рассмотрим сниппет и расширение, а CLI — в следующей.

Если вас интересует лишь исходный код, вот ссылка на репозиторий.

Сниппет (Snippet)


Что такое сниппет? Если коротко, сниппет — это заготовка, которую использует редактор для автозаполнения (автодополнения кода).

В VSCode встроен Emmet (официальный сайт, Emmet in Visual Studio Code), который использует многочисленные HTML, CSS и JS-сниппеты для помощи в написании кода. Набираем в редакторе (в .html) !, нажимаем Tab или Enter, получаем готовую html5-разметку. Набираем nav>ul>li*3>a.link>img, нажимаем Tab, получаем:

<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>

и т.д.

Кроме встроенных, VSCode предусматривает возможность использования пользовательских сниппетов. Для их создания необходимо перейти в File -> Preferences -> User Snippets (или нажать на кнопку Manage в левом нижнем углу и выбрать User Snippets). Настройки для каждого языка хранятся в соответствующем JSON-файле (для HTML в html.json, для JavaScript в javascript.json и т.д.).

Потренируемся создавать JS-сниппеты. Находим файл javascript.json и открываем его.



Видим комментарии, кратко описывающие правила создания сниппетов. Более подробную информацию о создании пользовательских сниппетов в VSCode можно найти здесь.

Начнем с чего-нибудь простого. Создадим сниппет для console.log(). Вот как он выглядит:

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

  • Print to console — ключ объекта, название сниппета (обязательно)
  • prefix — сокращение для сниппета (обязательно)
  • body — сам сниппет (обязательно)
  • $число — положение курсора после создания сниппета; $1 — первое положение, $2 — второе и т.д., $0 — последнее положение (опционально)
  • description — описание сниппета (опционально)

Сохраняем файл. Набираем log в скрипте, нажимаем Tab или Enter, получаем console.log() с курсором между скобками.

Создадим сниппет для цикла for-of:

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

  • Сниппеты, состоящие из нескольких строк, создаются с помощью массива
  • ${число: значение}; ${1:item} означает первое положение курсора со значением item по умолчанию; данное значение выделяется после создания сниппета, а также после перехода к следующему положению курсора для быстрого редактирования
  • \t — один отступ (величина оступа определяется соответствующими настройками редактора или, как в моем случае, расширения Prettier), \t\t — два отступа и т.д.

Набираем в скрипте fo, нажимаем Tab или Enter, получаем:

for (const item of arr) {

}

с выделенным item. Нажимаем Tab, выделяется arr. Еще раз нажимаем Tab, переходим на вторую строку.

Вот еще несколько примеров:

"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}",
    "}"
  ]
}

HTML-сниппеты строятся по такому же принципу. Вот как выглядит HTML Template:

{
  "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"
  }
}

Набираем html, нажимаем Tab или Enter, получаем разметку. Положения курсора определены в следующем порядке: название приложения (title), описание (description), автор (author), ключевые слова (keywords), адрес (url).

Расширение (Extension)


На сайте VSCode имеется отличная документация по созданию расширений.

Мы создадим два варианта расширения: в форме сниппетов и в форме CLI. Второй вариант опубликуем в Visual Studio Marketplace.

Примеры расширений в форме сниппетов:

  • JavaScript (ES6) code snippets
  • ES7 React/Redux/GraphQL/React-Native snippets
  • Vue VSCode Snippets

Расширения в форме CLI менее популярны, вероятно, по той причине, что существуют «настоящие» CLI.

Расширение в форме сниппетов

Для разработки расширений для VSCode, нам, кроме Node.js и Git, потребуется еще парочка библиотек, точнее, одна библиотека и плагин, а именно: yeoman и generator-code. Устанавливаем их глобально:

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

Выполняем команду yo code, выбираем New Code Snippets, отвечаем на вопросы.



Осталось скопировать созданный нами ранее HTML-сниппет в файл snippets/snippets.code-snippets (файлы сниппетов также могут иметь расширение json), отредактировать package.json и README.md, и можно публиковать расширение в маркетплейсе. Как видите, все очень просто. Слишком просто, подумал я, и решил создать расширение в форме CLI.

Расширение в форме CLI

Снова выполняем команду yo code. На этот раз выбираем New Extension (TypeScript) (не бойтесь, TypeScript в нашем коде почти не будет, а там, где будет, я дам необходимые разъяснения), отвечаем на вопросы.



Для того, чтобы убедиться в работоспособности расширения, открываем проект в редакторе:

cd htmltemplate
code .

Нажимаем F5 или на кнопку Run (Ctrl/Cmd+Shift+D) слева и кнопку Start Debugging сверху. Иногда при запуске можно получить ошибку. В этом случае отменяем запуск (Cancel) и повторяем процедуру.

В открывшемся редакторе нажимаем View -> Command Palette (Ctrl/Cmd+Shift+P), набираем hello и выбираем Hello World.



Получаем информационное сообщение от VSCode и соответствующее сообщение (поздравление) в консоли.



Из всех файлов, имеющихся в проекте, нас интересуют package.json и src/extension.ts. Директорию src/test и файл vsc-extension-quickstart.md можно удалить.

Заглянем в extension.ts (комментарии удалены для удобочитаемости):

// импорт функционала 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() {}

Важный момент: 'расширение.команда' в extension.ts должно совпадать со значениями полей activationEvents и command в package.json:

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

  • commands — список команд
  • activationEvents — функции, вызываемые при выполнении команд

Приступаем к разработке расширения.

Мы хотим, чтобы наше расширение по функционалу напоминало create-react-app или vue-cli, т.е. по команде create создавало проект, содержащий все необходимые файлы, в целевой директории.

Для начала отредактируем package.json:

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

Создаем директорию src/components для хранения файлов проекта, которые будут копироваться в целевую директорию.

Создаем файлы проекта в виде ES6-модулей (VSCode по умолчанию использует ES6-модули (export/import), но поддерживает и CommonJS-модули (module.exports/require)): index.html.js, css/style.css.js, script.js и т.д. Содержимое файлов экспортируется по умолчанию:

// 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>
`

Обратите внимание, что при таком подходе все изображения (в нашем случае, иконки) должны быть закодированы в Base64: вот один из подходящих онлайн-инструментов. Наличие строки «data:image/png;base64,» в начале преобразованного файла принципиального значение не имеет.

Для копирования (записи) файлов мы будем использовать fs-extra. Метод outputFile данной библиотеки делает томе самое, что и встроенный Node.js-метод writeFile, но также создает директорию для записываемого файла при ее отсутствии: например, если мы указали создать css/style.css, а директории css не существует, outputFile создаст ее и запишет туда style.css (writeFile при отсутствии директории выбросит исключение).

Файл extension.ts выглядит следующим образом:

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() {}

Для того, чтобы TypeScript не обращал внимания на отсутствие типов импортируемых файлов-модулей, создадим src/global.d.ts следующего содержания:

declare module '*'

Протестируем расширение. Открываем его в редакторе:

cd htmltemplate
code .

Запускаем отладку (F5). Переходим в целевую директорию (test-dir, например) и выполняем команду create в Command Palette.



Получаем информационное сообщение об успешном создании файлов. Ура!



Публикация расширения в Visual Studio Marketplace

Для того, чтобы иметь возможность публиковать расширения для VSCode, необходимо сделать следующее:

  • Создать аккаунт в маркетплейсе (запомните значение поля publisher)
  • Глобально установить библиотеку vsce

Редактируем 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"
      }
    ]
  },
  ...
}

Редактируем README.md.

Выполняем команду vsce package в директории расширения для создания публикуемого пакета с расширением vsix. Получаем файл htmltemplate-1.0.0.vsix.

На странице управления расширениями маркетплейса нажимаем кнопку New extension и выбираем Visual Studio Code. Переносим или загружаем в модальное окно VSIX-файл. Ждем завершения проверки.



После того, как рядом с номером версии появилась зеленая галочка, расширение становится доступным для установки в VSCode.



Для обновления расширения необходимо изменить номер версии в package.json, сгенерировать VSIX-файл и загрузить его в маркетплейс, нажав на кнопку More actions и выбрав Update.

Как видите, в создании и публикации расширений для VSCode нет ничего сверхестественного. На этом позвольте откланяться.

В следующей части мы создадим полноценный интерфейс командной строки сначала с помощью фреймворка от Heroku — oclif, затем без него. Наш Node.js-CLI будет сильно отличаться от расширения, в нем будет присутствовать некоторая визуализация, возможность опциональной инициализации git и установки зависимостей.

Надеюсь, вы нашли для себя что-нибудь интересное. Благодарю за внимание.
Источник: https://habr.com/ru/post/531312/


Интересные статьи

Интересные статьи

Часть 1 ‣ Часть 2 ‣ Часть 3 ‣ Часть 4 ‣ Часть 5 ‣ Часть 6 ‣ Часть 7 ‣ Часть 8 ‣ Часть 9 ‣ Часть 10 ‣ Часть 11 ‣ Часть 12 ‣ Часть 13 ‣ Часть 14 Правила предоставления услуг для Starl...
Диcклеймер — я практически не знаком с астрономией, только вот в Kerbal на орбиту выходил и как-то мне удалось сделать парочку орбитальных маневров. Но тема интересная, так, что даж...
Многие компании стараются заботиться о своих сотрудниках. Кому-то даже кажется, что они в этом преуспели, но на деле не всё так просто. Как компании оценить свой HR-бренд, а соискателю выбрать ...
В предыдущей части мы собрали микроконтроллер вообще без оперативной памяти на базе ПЛИС Altera/Intel. Однако на плате есть разъём с установленным SO-DIMM DDR2 1Gb, который, очевидно, хочется ис...
В 2017-2018 годах я искал работу в Европе и нашел в Нидерландах (про это можно прочитать здесь). Летом 2018-го мы с женой постепенно перебрались из Подмосковья в пригород Эйндховена и более-менее...