Tú, yo y package.json
Si has trabajado en algún proyecto en Node, o JS en el último tiempo, las posibilidades de haberte topado con un archivo llamado package.json son bastante altas. Al mirar dentro, si bien hay algunas cosas que podemos reconocer de manera inmediata, hay algunos valores y textos que es muy probable que no sepamos que significan ni como llegaron ahí.
Acompáñanos a un viaje rimbombante y mágico por el misterioso mundo de Node, en donde trataremos de cubrir los detalles necesarios para entender con claridad porque está ahí, de que nos sirve, y como sacarle el máximo provecho.
¿Qué es el package.json?
De cierta forma, podemos considerar este package.json como un manifiesto de nuestro proyecto.
Históricamente, Node ha trabajado con una herramienta para administrar paquetes llamada npm (o narwhals play music, de acuerdo a los calcetines de npm que alguna vez @fforres se ganó en una conferencia). Esta herramienta, que normalmente se instala junto con Node, tiene dos roles fundamentales:
- Manejar la publicación de un proyecto al registro público de npm (para que otros puedan descargarlo y utilizarlo como dependencia en sus propios proyectos).
- Administrar las dependencias de tu proyecto.
Para esto, guarda un registro en un archivo llamado, justamente, package.json.
Dentro de este archivo se definen y manejan características como:
- Nombre de tu proyecto.
- Versión.
- Dependencias.
- Repositorio.
- Autores.
- Licencia.
Y más.
(Adicionalmente, desde hace un tiempo, nuestros amigos personales de Facebook — aunque nosotros somos más amigos de ellos, que ellos de nosotros — , lanzaron una nueva herramienta de administración de paquetes para Node llamada yarn. Su funcionamiento, al menos en cuanto al uso del package.json, es prácticamente igual, por lo que al menos para efectos de este artículo, podemos considerar lo mismo para ambos, excepto en casos donde indiquemos explícitamente lo contrario.)
A través de este archivo, finalmente, se puede garantizar la integridad del proyecto. Es decir, podemos asegurar que quienes tengan una copia del mismo, podrán acceder a las mismas propiedades y sincronizar entre múltiples partes cada vez que decidan hacer un cambio. De cierta forma, podemos considerar este package.json como un manifiesto de nuestro proyecto.
Por ejemplo, consideremos el siguiente escenario:
Dos personas están trabajando en el mismo proyecto, con copias independientes en cada uno de sus equipos. El primero de ellos determina que para completar la nueva funcionalidad, va a necesitar implementar una nueva librería al proyecto.
Antiguamente, sin manejo de dependencias, era necesario hacer una de dos cosas:
- Incluir la librería (1 o múltiples archivo(s)) manualmente en el directorio del proyecto, potencialmente aumentando el peso del proyecto de manera considerable.
- No incluir la librería, pero comunicarle a cada persona que obtuviera una copia del mismo que antes de trabajar en el proyecto necesitaría instalar esta nueva dependencia (buena forma de hacer nuevos amigos, poco óptimo en términos de tiempo).
Con el uso de administradores de dependencias, ya estos pasos no son necesarios. Ahora cada persona que decida obtener una copia del proyecto, desde ahora al final de los tiempos, puede instalar todas y cada una de las dependencias que tengamos declaradas en este “manifiesto” sin la necesidad de incluir una copia de éstas en ningún otro lado más que ahí.
Cabe mencionar que si bien muchas características del package.json parecieran ser específicas para proyectos publicados en el registro de npm (como librerías), también aplican para proyectos cuya finalidad no es ser publicados en ningún registro (como por ejemplo aplicaciones Web o móviles, juegos y otros), pero que si se benefician de las utilidades relacionadas a la administración de dependencias.
¿Cómo crearlo?
One rule, to ring them all (?)
Antes de crear un package.json, hay solo una regla a tener en consideración: El archivo debe ser un JSON de formato válido (no puede ser un objeto literal de JS exportado desde un archivo), con todas las especificaciones que esto implica (por ejemplo, cada key debe tener comillas dobles, solo ciertos valores son válidos, etc.)
Más información sobre reglas y formatos permitidos.
Para crearlo, hay 2 formas: hacerlo de forma manual o hacerlo de forma automática:
Creando un package.json manualmente
Si bien es recomendable usar alguno de los asistentes para crear el archivo de forma automática, en caso de que necesitemos hacerlo de forma manual, es solo cosa de crear un archivo llamado package.json en la raíz del proyecto e incluir, como mínimo, la siguiente información:
- name.
- version.
Todos los demás campos son opcionales, aunque recomendados.
Creando un package.json automáticamente
Es la forma más rápida de hacerlo, ya que tanto npm como yarn incluyen un asistente que nos permite crear un package.json con un solo comando:
npm init
o bien
yarn init
Dependiendo de cual usemos, el asistente nos hará algunas preguntas para definir la información del proyecto (nombre, version, archivo de entrada, licencia y repositorio entre otros)
Al terminar, tendremos un nuevo y flamante package.json en la raíz del directorio donde hayamos ejecutado el comando.
Las Secciones del package.json
Al crear el package.json, ya bien de forma manual o automáticamente, vamos a encontrar dentro de el un gran objeto con múltiples keys y valores (como la imagen de cabecera de este artículo). Adicionalmente es muy probable, que con el tiempo, vayamos agregando nuevas keys y/o veamos en otros proyectos algunas de las cuales ni siquiera teníamos idea que existían. Para no dejar espacio a dudas, este es un listado de las keys que podemos agregar, que significa cada una, y algunas recomendaciones a tener en cuenta:
*Nota*: Estas son las propiedades oficiales soportadas por npm. Hay múltiples librerías que adicionalmente soportan incluir otros campos en el package.json y leen desde archivo (ej. Jest y la propiedad “jest”)
name
Uno de los dos campos obligatorios (junto a version). Es un string que representa el nombre del proyecto actual y forma un identificador único entre este campo y version en caso de que sea publicado al registro de npm.
Reglas:
- El nombre no puede contener mayúsculas ni empezar con un punto o guión bajo.
- El largo máximo del nombre es 214 caracteres, cada uno de los cuales debe ser URL safe (más información sobre caracteres URL safe en este documento, sección 2.3)
Algunas otras cosas a tener en consideración:
- Si queremos publicar el proyecto al registro de npm, hay que validar que no exista anteriormente un nombre con el mismo proyecto.
- Hay algunas buenas prácticas con respecto al formato de nombres de algunos proyectos que es bueno chequear. Por ejemplo, si bien no es considerado buena práctica incluir “node” o “js” en los paquetes, si se acostumbra incluir “react” en el nombre de paquetes orientados a esa tecnología.
version
Como el nombre lo indica, es un string con la versión actual del proyecto. Los paquetes/proyectos/librerías en Node y JS siguen las convenciones definidas en Semantic Versioning (o semver para los más amigos), la cual define la forma de versionamiento:
MAJOR.MINOR.PATCH
description
Un string que describa lo que hace este proyecto. En caso de que decidamos publicar en el registro de npm, este texto ayudará a la gente a encontrarlo mediante la búsqueda de npm.
keywords
Igual a description, pero en vez de texto, es un array de strings que incluye términos que puedan ser utilizados para una eventual búsqueda.
homepage
Es un string con la URL del proyecto.
bugs
Es un string con una URL válida donde se puedan reportar problemas con el proyecto. Usualmente el link de los issues del repositorio.
license
Es un string que especifica que tipo de licencia definimos para el uso de este proyecto, ya sea de forma personal, comercial, abierta y/o privada.
Más información sobre licencias disponibles.
author
Puede ser un string o un objeto con la información del creador del proyecto.
Si es un objeto, incluye las siguientes propiedades:
- name
- url
Si es un string, es en formato:
"Nombre <email> (url)"
contributors
Igual a author, pero en vez de un objeto o un string, es un array de alguno de estos 2, que incluye la información de colaboradores del proyecto.
files
Es un array de strings o patrones (ejemplo: *.js) que serán incluidos en caso de publicar el proyecto en el registro de npm. Si no incluimos esta sección, todos los archivos serán publicados, a excepción de los que tengamos definidos automáticamente para excluir (por ejemplo los definidos dentro del .gitignore).
Algunas consideraciones:
- Como alternativa a esta sección, se puede incluir un .npmignore que funciona de manera similar a un .gitignore dentro de un proyecto.
- En caso de no tener la sección files, ni un .npmignore, se tomará el contenido del .gitignore como referencia para excluir archivos.
- Algunos archivos serán siempre incluidos, independiente de lo que definamos: package.json, README, CHANGES / CHANGELOG / HISTORY, LICENSE / LICENCE, NOTICE y el archivo definido en la sección main del package.json (más detalles a continuación).
- Y tal como eso, hay algunos archivos que siempre serán ignorados, independiente de lo que definamos. Pueden encontrar una lista completa en este enlace.
main
Un string que define la ruta del archivo principal o punto de entrada de tu proyecto. Este es el archivo que una persona recibirá si importa tu proyecto al suyo. Por ejemplo:
Si tu proyecto o paquete se llama super-awesome-library y alguien lo instala y hace en uno de sus archivos
const super-awesome-library = require("super-awesome-library")
obtendrá el contenido del archivo que definamos en el main y se asignará a la constante.
bin
Un string (si es uno solo) o un objeto (si son múltiples) en el que podemos definir scripts que queremos instalar como ejecutables en el PATH. Al instalar el paquete, se creará un enlace simbólico desde /usr/local/bin hacia un archivo dentro de nuestro proyecto, y lo convertirá de esa forma en un ejecutable.
Por ejemplo, si tenemos un script llamado cli.js en nuestro proyecto y queremos que se convierta en un ejecutable, podemos agregarlo al package.json de la siguiente forma:
{
"name": "super-awesome-library",
"bin": "cli.js"
}
Si lo agregamos de esa forma (como un string solamente), este se va a agregar como un ejecutable utilizando el nombre del proyecto (en este caso “super-awesome-library”). Con esto, podríamos ejecutar desde la consola:
super-awesome-library
Y lo que estaría pasando por debajo es que en realidad se correría algo como:
node cli.js
Y se ejecutaría el contenido de este archivo.
Por otro lado, si tenemos múltiples archivos que queremos agregar como ejecutables, podemos agregar un objeto como bin, y este creará un enlace por cada propiedad del objeto, apuntando al script que dejemos como valor. Por ejemplo, si definimos:
{
"bin": {
"script-1": "script1.js",
"script-2": "script2.js"
}
}
Se agregarán tanto “script-1” como “script-2” al PATH, apuntando a sus respectivos archivos .js (Los nombres los podemos definir como estimemos conveniente, no es necesario que sean iguales).
Esto es lo que utilizan muchos paquetes conocidos, como nodemon o react-native, para que cuando los instalemos como dependencia los podamos ejecutar directamente, sin necesidad de incluir la ruta completa de donde están.
man
Un string, o un array de strings, que especifica uno (o muchos archivos) que se relacionarán a este proyecto si se corre el comando man en la máquina donde se haya instalado.
directories
Un objeto que especifica las rutas para la estructura de directorios del proyecto. Ejemplo:
{
"bin": "./bin",
"doc": "./doc",
"lib": "./lib"
}
repository
Un objeto que especifica el tipo y URL del repositorio donde está el código del proyecto. Se usa el siguiente formato:
{
"type": string,
"url": string
}
Ejemplo:
{
"type”: "git",
"url": "https://github.com/mi-usuario/mi-proyecto"
}
scripts
Es un objeto que indica comandos que podemos correr dentro de nuestro proyecto, asociándolos a una palabra clave para que npm (o yarn) los reconozca cuando queramos ejecutarlos.
Hay algunos scripts que vienen predefinidos en todos los proyectos al momento de utilizar npm, como son: start, install, preinstall, pretest, test y posttest entre otros (para una lista completa, pueden revisar este enlace).
De la misma forma, podemos definir algunos scripts personalizados y asociarlos al tipo de comandos que queramos, ahorrándonos recordar comandos completos que pueden ser simplificados al incluirlos acá.
Por ejemplo: Imaginemos que antes de lanzar una nueva versión de la aplicación en la que estamos trabajando, queremos ejecutar una tarea para minificar los archivos JS del proyecto, y esto lo hacemos mediante un script que tenemos en la ruta tasks/minify.js. Normalmente, lo que tendríamos que hacer, sería ejecutar cada vez que lo recordemos node tasks/minify.js y esperar que haga su trabajo. Sin embargo, si lo agregamos a los scripts de esta forma:
"scripts": {
"minify": "node tasks/minify.js"
}
Podemos ejecutar
npm run minify
y la tarea se va a ejecutar de la misma forma que si hubiésemos corrido el comando directamente. La gracia dentro de esto, es que en un mismo script definido en el package.json, podemos combinar múltiples comandos e incluso múltiples scripts definidos en el mismo package.json, con lo que podemos encadenar tareas y armar nuestros propios flujos de trabajo automatizados.
Como última nota, para correr un script definido en el package.json, debemos hacer npm run <script>, a no ser de que sea alguno de los scripts predefinidos (nombrados más arriba), los cuales se pueden correr directamente con npm <script>. Si están usando yarn, pueden omitir la palabra run completamente y solo ejecutar yarn <script>, independiente de si son predefinidos o no.
config
Es un objeto al que podemos pasarle valores y usarlos como variables de ambiente dentro de nuestro proyecto. Cualquier usuario que importe este proyecto al suyo, podrá reescribir esas variables con valores propios y utilizarlos de manera normal.
dependencies
Un objeto que guarda los nombres y versiones de cada dependencia que hemos instalado dentro del proyecto. De esta forma, cada vez que alguien obtenga una copia de este proyecto, y corra el comando npm install, se instalarán todas las dependencias que aquí estén definidas y por ende, no habrán problemas de compatibilidad al correr el proyecto. Estas dependencias, así como las de las siguientes categorías, se definen de la siguiente forma:
"nombre-de-la-dependencia": "(^|~|version)|url"
Algunos ejemplos:
"dependencies": {
"backbone": "1.0.0",
"lodash": "^4.6.1",
"mocha": "~3.5.3",
"super-mega-libreria": "https://noders.com/super-mega-libreria-4.0.0.tar.gz"
}
Las dependencias pueden o bien: llevar como valor la versión que están utilizando, o de una URL desde donde obtenerla (incluso una ruta local en la misma máquina), la cual por lo general apunta a una versión específica.
¿Qué son los símbolos ^ y ~, que acompañan al número de versión?
Son caracteres opcionales que definen como debe ser tratada esa dependencia la próxima vez que se corra npm install en el proyecto:
- Si la versión tiene un ^: Se buscará una versión compatible con la que está definida ahí.
- Si la versión tiene un ~: Se buscará una versión lo más cercana posible a la definida.
- Si no tiene ningún símbolo: Se instalará la misma versión.
Si bien estos 3 son los más comunes, hay algunos otros que también nos podemos encontrar por ahí. Para mayor información sobre todos los existentes, pueden revisar este enlace.
devDependencies
Mismo formato que las dependencias, pero acá podemos incluir todas aquellas librerías que no son necesarias para que este proyecto corra en producción, o cuando sea requerido e instalado dentro de otro. Con esto le ahorramos a quien importe este proyecto de tener dependencias instaladas que pueden no ser necesarias para que esto funcione (como por ejemplo herramientas de tests).
peerDependencies
Mismo formato anterior, pero acá definimos dependencias que son necesarias para el uso del proyecto, aún cuando no las tengamos instaladas dentro del proyecto en si. De esta forma, podemos asegurar que se cumplan ciertas reglas de compatibilidad. Por ejemplo, podemos definir que este proyecto es solo compatible con la versión 16 de react:
{
"peerDependencies: {
"react": "16.0.0"
}
}
De esta forma se hará un chequeo al momento de importar este proyecto que las condiciones estén cumplidas.
engines
Un objeto en el cual podemos definir la versión mínima de node y npm necesarias para correr este proyecto. La definimos de la forma:
"engines": {
"node": "≥ 6.0.0",
"npm": "≥ 3.0.0"
}
Al momento de instalar el proyecto, se verificará que versiones están instaladas actualmente, y de no cumplir el requisito, no se continuará el proceso de instalación.
Al igual que con las dependencias, podemos usar ~ y ^ junto al número de versión.
Más información
Existen algunos comandos extras (menos usados), que también podemos incluir en nuestro package.json. Para mayor referencia, es recomendable revisar la documentación oficial de npm de manera constante, ya que está siendo constantemente actualizada, pasando algunos a deprecación e incluyendo nuevos a tener en cuenta.
Algunos links de referencia que pueden ser útiles para esto:
Noders es la comunidad más electrizante de Node.js y JavaScript de Latinoamérica. Generamos iniciativas de aprendizaje e integración de comunidades de desarrollo en diferentes países y a lo largo y ancho de esta cosita llamada la Internet.
Si quieres saber más de nosotros y/o participar de lo que hacemos, únete a nuestro Slack.
Gracias a Felipe Andrés Torres Sepu, Ender Bonnet y Nicolás Avila por las revisiones y correcciones de este artículo.