Foto por Sigmund on Unsplash

En este segundo artículo de la serie sobre las distintas herramientas orientadas a la experiencia de desarrollo revisaremos que es npm.

Puedes encontrar el primer artículo: ¿Qué es Linting y ESLint? en este enlace.

¿Qué es npm?

npm es parte esencial de Node.js, el entorno de ejecución de javaScript en el lado del servidor basado en el motor V8 de Google. Es muy seguramente la principal razón del gran éxito de Node permitiendo que cientos de desarrolladores puedan compartir paquetes de software entre distintos proyectos.

En este momento, se han publicado 1,493,231 paquetes por medio de su repositorio con más de 27 mil millones de descargas.

npm responde a las siglas de Node Package Manager o manejador de paquetes de node, es la herramienta por defecto de JavaScript para la tarea de compartir e instalar paquetes.

Tal como reza su documentación, npm se compone de al menos dos partes principales.

  • Un repositorio online para publicar paquetes de software libre para ser utilizados en proyectos Node.js
  • Una herramienta para la terminal (command line utility) para interactuar con dicho repositorio que te ayuda a la instalación de utilidades, manejo de dependencias y la publicación de paquetes.

Es decir, en tu proyecto basado en Node — que actualmente incluye los proyectos de aplicaciones web que utilizan Node para su proceso de compilación y generación de archivos — utilizarás la utilidad de linea de comandos (cli) para consumir paquetes desde el repositorio online, un listado gigantesco de soluciones de software para distintos problemas disponibles públicamente en npmjs.com y para manejar dependencias, y para ello necesitas un archivo de configuración que le diga a npm que este es un proyecto node.

package.json

Este archivo indica a npm que el directorio en el que se encuentra es en efecto un proyecto o paquete npm. Este archivo contiene la información del paquete incluyendo la descripción del mismo, versión, autor y más importante aún dependencias.

Este archivo es generado automáticamente mediante la ejecución de un script de npm: npm init este script es ejecutao para inicializar un proyecto JavaScript, al ejecutarlo la linea de comandos te hará algunas preguntas para crear el paquete:

 $ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (test)
Esta utilidad te guiará en el proceso de crear un archivo package.json. Solo cubre los items más comunes y trata de adivinar algunos valores por defecto. Revisa npm help init para documentación más a fondo sobre estos campos y que hacen exactamente. Utiliza npm install <pkg> para instalar paquetes y guardarlos como dependencia en el archivo package.json. Presiona ^C en cualquier momento para cancelar
nombre del paquete: (que-es-npm)

Lo primero que verás al ejecutar npm init es un mensaje como el anterior y la primera pregunta: nombre del paquete: (que-es-npm) que entre paréntesis mostrará el nombre del directorio en el que se está ejecutando como nombre por defecto.

Luego veras lo siguiente.

version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository:
author:
license (MIT):
private:

Puedes simplemente presionar Enter en cada una de estas opciones para dejar vació el campo o guardar el valor por defecto que se muestra entre paréntesis.

  • version: Corresponde a la versión de tu proyecto. Lo ideal es mantener este campo actualizado cuando modificas algo en tu proyecto o librería utilizando por ejemplo semver.
  • description: Una breve descripción de tu proyecto. Particularmente importante si lo que estás creando es un paquete que publicarás vía npm.
  • entry point: Define cuál será el punto de "entrada" de tu proyecto. Esto es, que archivo se ejecutará cuando se importe tu proyecto dentro de otro. Nuevamente, especialmente importante para paquetes de librerías.
  • test command: Aquí puedes definir el comando que quieres ejecutar para realizar las pruebas de tu proyecto, este comando se ejecutará cuando escribas npm run test en tu terminal.
  • git repository: Define la url del repositorio git en donde este proyecto está alojado, se utiliza para informar a los usuarios de tu paquete el repositorio en donde encontrar el código fuente del proyecto.
  • author: El nombre e email de quien creó este proyecto.
  • license: Identifica el tipo de licencia de uso de tu proyecto. Permite a las personas saber que y que no está permitido al usar tu código. Puedes encontrar la lista completa de licencias soportadas por este campo aquí.
  • private: Es un valor boolean que te permite evitar que tu paquete se publique en el repositorio. Si lo que estás creando es un proyecto personal este valor será true.

Una vez contestadas todas las preguntas y terminado el proceso de inicialización tendrás un nuevo archivo dentro del directorio de tu proyecto: project.json cuyo contenido será similar a este

{
  "name": "que-es-npm",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \\"Error: no test specified\\" && exit 1"
  },
  "author": "Matias Hernandez <matiasfh@gmail.com> (<https://matiashernandez.dev>)",
  "license": "ISC"
}

npm scripts

Una importante sección de este archivo es scripts. Esta sección define un listado de propiedades que permiten ejecutar comandos dentro del contexto de tu proyecto incluyendo: comandos de otros paquetes listados como dependencias, scripts personalizados, scripts bash, etc.

Por defecto se crea un script para ejecutar el comando de test que, si no agregaste nada personalizado en el proceso de inicialización sólo tendrá una llamada al comando echo, es decir, ****al ejecutar en la terminal npm run test verás en la consola Error: no test specified

Un ejemplo de producción de esta sección es:

{
		...
		...
    "scripts": {
        "start": "npm run generate && PORT=5000 react-scripts start",
        "build": "react-scripts build",
        "storybook": "start-storybook -p 6006 -h localhost",
        "build-storybook": "build-storybook -s public",
        "test": "jest",
        "test:watch": "jest --watch --silent",
        "cypress": "cypress run",
        "eject": "react-scripts eject",
        "lint": "eslint src/**/*.{js,ts,tsx} --fix",
        "prettier": "npx prettier --write '**/*.tsx'",
        "generate": "graphql-codegen --config codegen.yml"
    },
    ...
		...
}

En este ejemplo podemos ver una lista de 11 scripts que cumplen diferentes tareas tales como:

  • start: Primero ejecuta el script generate y luego inicia la aplicación web. react-scripts es una dependencia del paquete y se encuentra disponible dentro del directorio node_modules.
  • build: Ejecuta la compilación de la aplicación en modo producción.
  • lint: Ejecuta el proceso de linting (revisión de formato y estilo de código) del proyecto (Puedes leer más sobre este proceso en el primer artículo de esta colección)
  • test: Ejecuta el script de pruebas basado en el paquete jest que también está instalado como dependencia.
  • prettier: Ejecuta prettier mediante el uso de npx. Este es un paquete especial de npm que permite ejecutar binarios dentro del alcance de tu proyecto sin necesidad de especificar dicho comando como script dentro del archivo package.json.

Dependencias y dependencias de desarrollo

La siguiente sección muy relevante dentro del archivo es el listado de dependencias y el listado de dependencias de desarrollo

{
   
    "dependencies": {
        "react": "^17.0.1",
        "react-dom": "^17.0.1",
        "react-hook-form": "6.14.1",
        "react-i18next": "11.8.5",
        "react-query": "^3.5.12",
        "react-router-dom": "5.2.0",
        "react-scripts": "4.0.1",
        "react-table": "^7.6.3",
        "react-virtual": "2.3.2",
        "yup": "0.32.8"
    },
		...
		...
    "devDependencies": {
        "@emotion/jest": "11.1.0",
        "@graphql-codegen/add": "2.0.2",
        "@graphql-codegen/cli": "1.20.1",
        "@graphql-codegen/introspection": "1.18.1",
        "@graphql-codegen/typescript": "1.20.2",
        "@graphql-codegen/typescript-graphql-request": "3.0.1",
        "@graphql-codegen/typescript-operations": "1.17.14",
        "@types/jest": "^26.0.20",
        "@types/node": "^12.0.0",
        "@types/react": "^16.9.53",
        "@types/react-dom": "^16.9.8",
        "@types/react-router-dom": "^5.1.7",
        "cypress": "^6.2.1",
        "eslint": "^7.17.0",
        "eslint-config-prettier": "^7.1.0",
        "eslint-plugin-prettier": "^3.3.1",
        "starwars-names": "1.6.0",
        "stylelint": "13.8.0",
        "stylelint-config-prettier": "8.0.2",
        "stylelint-prettier": "1.1.2",
        "ts-jest": "^26.4.4"
    }
}

Estas secciones, definen que paquetes disponibles en el repositorio de npm son requeridos por tu proyecto indicando también la versión necesaria. La versión mostrada aquí está en formato semver y corresponde al campo versionmencionado antes.

Estas dependencias son instaladas al ejecutar npm install <pkg> --save y en el caso de las dependencias de desarrollo utilizando npm install <pkg> --save-dev.

La diferencia de estos listados es que dependencies está destinado a ser utilizando en producción y devDependenciesdefine paquetes que son necesarios sólo durante el desarrollo de tu proyecto.

Es importante conocer cómo se definen las versiones a utilizar en estas dependencias. Cada una de ellas muestra un número basado en semver en la forma mayor.minor.patch.

  • mayor: Representa una versión mayor que genera cambios en la API del producto.
  • minor: Representa un valor que aumenta cuando se hacen cambios retro-compatibles.
  • patch: Un valor que aumenta cada vez que se hacen reparaciones de errores o mejoras sutiles.

También es posible encontrar algunos símbolos frente a la numeración de la versión, estos son:

  • ^: latest minor release. Por ejemplo ^1.0.4 indica que 1.0.4 es la versión más "baja" que se puede instalar pero permite que se instale cualquier versión superior a esa pero que se encuentre dentro de la versión 1.
  • ~: latest patch release. Esta es la forma contraria a  ^. Esta especificación  ~1.0.4 puede instalar la versión 1.0.7 si es que esta es la ultima version del patch.

Una vez instalados los paquetes de tus dependencias la información de las versiones instaladas queda almacenada en un archivo llamado package-lock.json

package-lock.json

Este archivo es auto generado por npm install y es una lista descriptiva y exacta de las versiones instaladas durante tu proceso. No esta destinado a ser leído ni manipulado por los desarrolladores, si no, para ser un insumo del proceso de manejo de dependencias.

¿Cómo trabajar con npm?

Lo más usual que harás con npm es la instalación de dependencias, esto se hace mediante npm install para instalar todas las dependencias listadas en el archivo package.json o utilizando npm install <pkg> para instalar algún paquete en particular.

npm install

Este script nativo de npm tiene algunas otras opciones a la hora de hacer la instalación de paquetes.

Por defecto al ejecutar npm install <pkg> la última versión disponible en el repositorio agregando el símbolo ^ a la versión.

Al ejecutar npm install <pkg> el paquete se instalará dentro del directorio node_modules que está dentro de tu proyecto.

Puedes usar algunos parámetros tales como

  • -g para indicar que quieres que el paquete se instale globalmente
  • --production indica que la ejecución de npm install solo instalará las dependencias listadas en el apartado dependencies dejando de lado las dependencias de desarrollo.

npm audit

npm tiene una miriada de paquetes disponibles lo que es una gran característica indicando lo saludable del ecosistema que es capaz de generar nuevas librerías a gran velocidad, pero también puede ser un problema. npm puede albergar paquetes maliciosos o con problema de seguridad.

La organización trás npm mantiene una lista de agujeros de seguridad y puedes utilizar este comando para revisar tus dependencias.

npm audit te entregará información de las vulnerabilidades encontradas en tus dependencias junto con una breve descripción de como resolverlo indicando la versión que corrige el defecto.

npm publish

En el caso de que tu proyecto sea una librería de software libre que quieres compartir con otros, este comando será el que te permitirá publicar tu paquete en el repositorio, solo necesitas ejecutar:

  • npm login para ingresar a tu cuenta de npm
  • npm publish para subir tu paquete al repositorio

Ten en cuenta que necesitas tener bien configurados tu entry point y el script de build.

También puedes hacer uso de los scripts del ciclo de vida. Una serie de scripts que se ejecutan en diferentes momentos del proceso de publicación:

  • prepare (desde npm@4.0.0): Se ejecuta antes de que el paquete se empaquete (packed) y antes de que se publique. También se ejecuta al correr npm install y después del script prepublishOnly
  • prepublishOnly: Se ejecuta antes de que el paquete sea preparado y empaquetado y solo cuando se ejecuta npm publish
  • prepack: Se ejecuta antes de empaquetar, es decir antes de npm pack y npm publish
  • postpack: Se ejecuta después de que el paquete fuese generado y ubicado en su destino final.

¿Cuándo usar estos scripts?

Cuando necesitas que tu paquete ejecuta alguna acción antes de ser utilizado y que no dependa del sistema operativo en que está siendo instalado, por ejemplo: transpilar tu código Typescript a JavaScript, obtener datos remotos que tu paquete necesita, etc.

¿Qué scripts se ejecutan al publicar un paquete?

Cuando ejecutas npm publish una serie de scripts son ejecutados automáticamente por npm el orden de estos scripts es el siguiente:

  • prepublishOnly
  • prepare
  • prepublish
  • publish
  • postpublish

Es decir, si necesitas ejecutar algún comando antes de que se publique tu paquete debes configurar el script prepublishOnly ¿Cómo?

{
	"scripts": {
		"prepublishOnly": "compile-my-code src/"
	}
}

Conclusión

npm es la solución que ofrece el ecosistema de JavaScript para manejar dependencias, auditar paquetes y mantener un repositorio de paquetes disponible para todos los usuarios, es la herramienta central de todo proyecto JavaScript sea este backend o frontend.

El sistema de npm se basa en un archivo centralizado que describe los metadatos del proyecto, scripts y dependencias tanto de producción como de desarrollo.