Snippet, una extensión para VSCode y CLI. Parte 1





¡Buen dia amigos!



Mientras desarrollaba la plantilla de inicio HTML moderna, pensé en ampliar su usabilidad. En ese momento, las opciones para su uso se limitaban a clonar el repositorio y descargar el archivo. Así es como aparecieron el fragmento HTML y la extensión para Microsoft Visual Studio Code - Plantilla HTML , así como la interfaz de línea de comandos - create-modern-template . Por supuesto, estas herramientas están lejos de ser perfectas y las perfeccionaré tanto como pueda. Sin embargo, en el proceso de crearlos, aprendí algunas cosas interesantes que quiero compartir con ustedes.



En esta parte, veremos el fragmento y la extensión, y la CLI en la siguiente.



Si solo está interesado en el código fuente, aquí está el enlace al repositorio .



Retazo



¿Qué es un fragmento? En resumen, un fragmento es una plantilla que el editor usa para autocompletar (completar el código).



VSCode ha incorporado Emmet ( sitio oficial , Emmet en Visual Studio Code ), que utiliza numerosos fragmentos de HTML, CSS y JS para ayudarlo a escribir su código. Escribimos en el editor (en .html) !, presionamos Tab o Enter, obtenemos el marcado html5 terminado. Escribimos nav> ul> li * 3> a.link> img, presionamos Tab, obtenemos:



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



Además de los integrados, VSCode ofrece la posibilidad de utilizar fragmentos personalizados. Para crearlos, vaya a Archivo -> Preferencias -> Fragmentos de usuario (o haga clic en el botón Administrar en la esquina inferior izquierda y seleccione Fragmentos de usuario). La configuración de cada idioma se almacena en un archivo JSON correspondiente (para HTML en html.json, para JavaScript en javascript.json, etc.).



Practiquemos la creación de fragmentos de código JS. Busque el archivo javascript.json y ábralo.







Vemos comentarios que describen brevemente las reglas para crear fragmentos. Puede encontrar más información sobre cómo crear fragmentos personalizados en VSCode aquí .



Comencemos con algo simple. Creemos un fragmento para console.log (). Así es como se ve:



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

      
      





  • Imprimir en la consola: clave de objeto, nombre del fragmento (obligatorio)
  • prefijo: abreviatura de fragmento (obligatorio)
  • cuerpo: el fragmento en sí (obligatorio)
  • $ number - posición del cursor después de la creación del fragmento; $ 1 - primera posición, $ 2 - segundo, etc., $ 0 - última posición (opcional)
  • descripción: descripción del fragmento (opcional)


Guardamos el archivo. Escribimos log en el script, presionamos Tab o Enter, obtenemos console.log () con el cursor entre corchetes.



Creemos un fragmento para el bucle for-of:



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

      
      





  • Los fragmentos de varias líneas se crean mediante una matriz
  • $ {número: valor}; $ {1: item} significa la primera posición del cursor con el valor del elemento predeterminado; este valor se resalta después de crear un fragmento, así como después de moverse a la siguiente posición del cursor para una edición rápida
  • \ t - una sangría (la cantidad de espacio está determinada por la configuración del editor correspondiente o, en mi caso, la extensión Prettier ), \ t \ t - dos sangrías, etc.


Escribimos fo en el script, presionamos Tab o Enter, obtenemos:



for (const item of arr) {

}

      
      





con el elemento resaltado. Presione Tab, arr está resaltado. Presione Tab nuevamente, vaya a la segunda línea.



A continuación se muestran algunos ejemplos más:



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

      
      





Los fragmentos de HTML siguen el mismo principio. Así es como se ve la plantilla 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"
  }
}

      
      





Escribimos html, presionamos Tab o Enter, obtenemos el marcado. Las posiciones del cursor se definen en el siguiente orden: nombre de la aplicación (título), descripción (descripción), autor (autor), palabras clave (palabras clave), dirección (url).



Extensión



El sitio de VSCode tiene una excelente documentación sobre la creación de extensiones .



Crearemos dos opciones para la extensión: formulario de fragmento y formulario CLI. Publicaremos la segunda opción en Visual Studio Marketplace .



Ejemplos de extensiones en forma de fragmentos:





Las extensiones de formulario CLI son menos populares, probablemente porque existen CLI "reales".



Extensión en forma de fragmentos


Para desarrollar extensiones para VSCode, además de Node.js y Git , necesitamos un par de bibliotecas más, más precisamente, una biblioteca y un complemento, a saber, yeoman y generator-code . Instálelos globalmente:



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

      
      





Ejecutamos el comando yo code, seleccionamos New Code Snippets, respondemos preguntas.







Queda por copiar el fragmento de HTML que creamos anteriormente en el archivo snippets / snippets.code-snippets (los archivos de fragmentos también pueden tener la extensión json), editar el package.json y README.md, y puede publicar la extensión en el mercado. Como ves, todo es muy sencillo. Demasiado simple, pensé, y decidí crear una extensión en forma de CLI.



Extensión CLI


Ejecute el comando del código yo de nuevo. En esta ocasión seleccionamos Nueva Extensión (TypeScript) (no temas, casi no habrá TypeScript en nuestro código, y donde esté, daré las explicaciones necesarias), responde las preguntas.







Para asegurarse de que la extensión funcione, abra el proyecto en el editor:



cd htmltemplate
code .

      
      





Presione F5 o el botón Ejecutar (Ctrl / Cmd + Shift + D) a la izquierda y el botón Iniciar depuración en la parte superior. A veces aparece un error al iniciar. En este caso, cancele el lanzamiento (Cancelar) y repita el procedimiento.



En el editor que se abre, haga clic en Ver -> Paleta de comandos (Ctrl / Cmd + Shift + P), escriba hola y seleccione Hola mundo.







Recibimos un mensaje informativo de VSCode y un mensaje correspondiente (felicitaciones) en la consola.







De todos los archivos del proyecto, estamos interesados ​​en package.json y src / extension.ts. El directorio src / test y el archivo vsc-extension-quickstart.md se pueden eliminar.



Echemos un vistazo a extension.ts (comentarios eliminados para facilitar la lectura):



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

      
      





Punto importante: 'extension.command' en extension.ts debe coincidir con los valores de los eventos de activación y los campos de comando en package.json:



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

      
      





  • comandos - lista de comandos
  • ActivaciónEventos: funciones que se llamarán durante la ejecución del comando


Comencemos a desarrollar la extensión.



Queremos que nuestra extensión se parezca a create-react-app o vue-cli en funcionalidad , es decir, en el comando crear creó un proyecto que contiene todos los archivos necesarios en el directorio de destino.



Primero, editemos package.json:



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

      
      





Cree un directorio src / components para almacenar los archivos del proyecto que se copiarán en el directorio de destino.



Creamos archivos de proyecto en forma de módulos ES6 (VSCode usa módulos ES6 por defecto (exportar / importar), pero admite módulos CommonJS (module.exports / require)): index.html.js, css / style.css.js , script.js, etc. El contenido de los archivos se exporta de forma predeterminada:



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

      
      





Tenga en cuenta que con este enfoque todas las imágenes (en nuestro caso, los iconos) deben estar codificadas en Base64: aquí hay una herramienta en línea adecuada . La presencia de la línea "data: image / png; base64" al principio del archivo convertido no tiene importancia fundamental.



Usaremos fs-extra para copiar (escribir) archivos . El método outputFile de esta biblioteca hace lo mismo que el método writeFile incorporado de Node.js, pero también crea un directorio para el archivo que se está escribiendo si no existe: por ejemplo, si especificamos create css / style.css, y el directorio css no existe, outputFile lo creará y escribirá style.css allí (writeFile lanzará una excepción si no hay un directorio).



El archivo extension.ts se ve así:



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 TypeScript preste atención a la falta de tipos de archivos de módulo importados, cree src / global.d.ts con el siguiente contenido:



declare module '*'

      
      





Probemos la extensión. Ábrelo en el editor:



cd htmltemplate
code .

      
      





Inicie la depuración (F5). Vaya al directorio de destino (test-dir, por ejemplo) y ejecute el comando de creación en la paleta de comandos.







Recibimos un mensaje informativo sobre la creación exitosa de archivos. ¡Hurra!







Publicar una extensión en Visual Studio Marketplace


Para poder publicar extensiones para VSCode, debe hacer lo siguiente:





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.



Ejecute el comando del paquete vsce en el directorio de extensiones para crear un paquete publicado con la extensión vsix. Obtenemos el archivo htmltemplate-1.0.0.vsix.



En la página de administración de extensiones del mercado, haga clic en el botón Nueva extensión y seleccione Visual Studio Code. Transfiera o cargue el archivo VSIX en la ventana modal. Estamos esperando que se complete la verificación.







Después de que aparezca una marca de verificación verde junto al número de versión, la extensión estará disponible para su instalación en VSCode.







Para actualizar la extensión, debe cambiar el número de versión en package.json, generar un archivo VSIX y cargarlo en el mercado haciendo clic en el botón Más acciones y seleccionando Actualizar.



Como puede ver, no hay nada sobrenatural en la creación y publicación de extensiones para VSCode. Sobre esto, déjame despedirme.



En la siguiente parte, crearemos una interfaz de línea de comandos completa, primero usando el marco Heroku - oclif , luego sin él. Nuestro Node.js-CLI será muy diferente de una extensión, tendrá algo de visualización, la capacidad de inicializar opcionalmente git e instalar dependencias.



Espero que hayas encontrado algo interesante para ti. Gracias por su atención.



All Articles