Cómo escribir, probar y publicar un paquete NPM

Cómo crear su propio paquete, escribir pruebas, ejecutar el paquete localmente y publicarlo en NPM.

Primeros pasos

Para este tutorial, querrá asegurarse de tener instalado Node.js (se recomienda la última versión de LTS, al momento de escribir, 16.13.1) en su computadora. Si no ha instalado Node.js antes, lea primero este tutorial.

Configuración de un proyecto

Para comenzar, configuraremos una nueva carpeta para nuestro paquete en nuestra computadora.

Terminal

mkdir package-name

A continuación, queremos cd en esa carpeta y crea un package.json archivo:

Terminal

cd package-name && npm init -f

Aquí, npm init -f le dice a NPM (Node Package Manager, la herramienta que usaremos para publicar nuestro paquete) para inicializar un nuevo proyecto, creando un package.json archivo en el directorio donde se ejecutó el comando. El -f significa "fuerza" y le dice a NPM que escupa una plantilla package.json expediente. Si excluye el -f , NPM te ayudará a crear el package.json archivo utilizando su asistente paso a paso.

Una vez que tenga un package.json archivo, a continuación, queremos hacer una pequeña modificación en el archivo. Si lo abre, queremos agregar un campo especial type al objeto establecido en un valor de "módulo" como una cadena, así:

{
  "type": "module",
  "name": "@cheatcodetuts/calculator",
  "version": "0.0.0",
  "description": "",
  "main": "./dist/index.js",
  "scripts": { ... },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": { ... }
}

En la parte superior del objeto JSON, agregamos "type": "module" . Cuando se ejecuta nuestro código, esto le dice a Node.js que esperamos que el archivo use la sintaxis del Módulo ES (Módulo ECMAScript o ESM para abreviar) en lugar de la sintaxis de Common JS. ESM utiliza el moderno import y export sintaxis mientras que CJS usa el require() declaración y module.exports sintaxis. Preferimos un enfoque moderno, por lo que al establecer "type": "module" , habilitamos soporte para usar import y export en nuestro código.

Después de esto, a continuación, queremos crear dos carpetas dentro de nuestra carpeta de paquetes:src y dist .

  • src contendrá los archivos "fuente" para nuestro paquete.
  • dist contendrá los archivos creados (compilados y minificados) para nuestro paquete (esto es lo que otros desarrolladores cargarán en su aplicación cuando instalen nuestro paquete).

Dentro del src directorio, queremos crear un index.js expediente. Aquí es donde escribiremos el código para nuestro paquete. Más tarde, veremos cómo tomamos este archivo y lo construimos, generando automáticamente la copia compilada en dist .

/src/index.js

export default {
  add: (n1, n2) => {
    if (isNaN(n1) || isNaN(n2)) {
      throw new Error('[calculator.add] Passed arguments must be a number (integer or float).');
    }

    return n1 + n2;
  },
  subtract: (n1, n2) => {
    if (isNaN(n1) || isNaN(n2)) {
      throw new Error('[calculator.subtract] Passed arguments must be a number (integer or float).');
    }

    return n1 - n2;
  },
  multiply: (n1, n2) => {
    if (isNaN(n1) || isNaN(n2)) {
      throw new Error('[calculator.multiply] Passed arguments must be a number (integer or float).');
    }

    return n1 * n2;
  },
  divide: (n1, n2) => {
    if (isNaN(n1) || isNaN(n2)) {
      throw new Error('[calculator.divide] Passed arguments must be a number (integer or float).');
    }

    return n1 / n2;
  },
};

Para nuestro paquete, vamos a crear una calculadora simple con cuatro funciones:add , subtract , multiply y divide y cada uno acepta dos números para realizar su función matemática respectiva.

Las funciones aquí no son muy importantes (esperemos que su funcionalidad sea clara). Lo que realmente quiero prestar atención es el export default en la parte superior y el throw new Error() líneas dentro de cada función.

Tenga en cuenta que en lugar de definir cada una de nuestras funciones individualmente, las hemos definido en un solo objeto que se exporta desde nuestro /src/index.js expediente. El objetivo aquí es tener nuestro paquete importado en una aplicación como esta:

import calculator from 'package-name';

calculator.add(1, 3);

Aquí, el objeto que se exporta es calculator y cada función (en JavaScript, las funciones definidas en un objeto se denominan "métodos") se accede a través de ese objeto como vemos arriba. Nota :así es como queremos que se comporte nuestro paquete de ejemplo, pero su paquete puede comportarse de manera diferente; esto es todo a modo de ejemplo.

Centrándose en el throw new Error() declaraciones, tenga en cuenta que estos son todos casi idénticos. El objetivo aquí es decir "si el n1 argumento o el n2 los argumentos no se pasan como números (enteros o flotantes), genera un error".

¿Por qué estamos haciendo esto? Bueno, considere lo que estamos haciendo:estamos creando un paquete para que otros lo usen. Esto es diferente de cómo podríamos escribir nuestro propio código donde las entradas son predecibles o controladas. Al desarrollar un paquete, debemos ser conscientes del posible uso indebido de ese paquete. Podemos dar cuenta de esto de dos maneras:escribiendo una documentación realmente buena, pero también haciendo que nuestro código sea tolerante a fallas e instructivo.

Aquí, debido a que nuestro paquete es una calculadora, podemos ayudar al usuario a usar el paquete correctamente al tener un requisito estricto de que nos pase números para realizar operaciones matemáticas. Si no lo hacen, les damos una pista sobre lo que hicieron mal y cómo solucionar el problema a nivel de código . Esto es importante para la adopción del paquete. Cuanto más fácil de usar sea su código para desarrolladores, más probable es que otros utilicen su paquete.

Avanzando más en este punto, a continuación, aprenderemos cómo escribir algunas pruebas para nuestro paquete y cómo ejecutarlas.

Pruebas de escritura para el código de su paquete

Queremos tener la mayor confianza posible en nuestro código antes de ponerlo a disposición de otros desarrolladores. Si bien podemos confiar ciegamente en lo que hemos escrito como funcional, esto no es sabio. En su lugar, antes de lanzar nuestro paquete, podemos escribir pruebas automatizadas que simulen a un usuario correctamente (o incorrectamente) usando nuestro paquete y asegurarnos de que nuestro código responda como esperábamos.

Para escribir nuestras pruebas, vamos a usar la biblioteca Jest de Facebook. Jest es una herramienta única que combina:

  • Funcionalidad para crear conjuntos de pruebas y pruebas individuales.
  • Funcionalidad para realizar aserciones dentro de las pruebas.
  • Funcionalidad para ejecutar pruebas.
  • Funcionalidad para informar los resultados de las pruebas.

Tradicionalmente, estas herramientas están disponibles para nosotros a través de múltiples paquetes independientes. Jest facilita la configuración de un entorno de prueba al combinarlos todos juntos. Para agregar Jest a nuestro propio paquete, necesitamos instalar sus paquetes a través de NPM (¡meta!):

Terminal

npm install -D jest jest-cli

Aquí, decimos que instales jest y su jest-cli paquete (el último es la interfaz de línea de comandos que usamos para ejecutar pruebas) como dependencias solo de desarrollo (pasando el -D marcar a npm install ). Esto significa que solo tenemos la intención de usar Jest en el desarrollo y no queremos que se agregue como una dependencia que se instalará junto con nuestro propio paquete en el código de nuestro usuario.

/paquete.json

{
  "type": "module",
  "name": "@cheatcodetuts/calculator",
  "version": "0.0.0",
  "description": "",
  "scripts": {
    "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
    "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "jest": "^27.4.3",
    "jest-cli": "^27.4.3",
  }
}

Ahora a profundizar en los detalles. Aquí, en nuestro package.json archivo, queremos agregar dos líneas a nuestro scripts objeto. Estos scripts se conocen como "scripts de NPM" que son, como su nombre lo indica, scripts de línea de comando reutilizables que podemos ejecutar usando el npm run de NPM función en la terminal.

Aquí, estamos agregando test y test:watch . El primer script se usará para ejecutar nuestras pruebas una vez y generar un informe mientras test:watch ejecutará nuestras pruebas una y otra vez cada vez que cambie un archivo de prueba (o código relacionado). El primero es útil para una revisión rápida de las cosas antes de la implementación y el segundo es útil para ejecutar pruebas durante el desarrollo.

Mirando de cerca el test guión node --experimental-vm-modules node_modules/jest/bin/jest.js estamos ejecutando esto de una manera extraña. Por lo general, podríamos escribir nuestro script como nada más que jest (literalmente, "test": "jest" ) y funcionaría, sin embargo, porque nos gustaría escribir nuestras pruebas usando Módulos ES (a diferencia de Common JS), necesitamos habilitar esto en Jest, tal como lo hicimos aquí en nuestro package.json para nuestro código de paquete.

Para hacer eso, necesitamos ejecutar Jest directamente a través de Node.js para que podamos pasar el --experimental-vm-modules marca a Node.js (requerido por Jest ya que las API que usan para implementar el soporte de ESM aún lo consideran una función experimental).

Porque estamos usando Node para ejecutar Jest (y no el jest-cli de jest comando directamente), también necesitamos apuntar a la versión binaria de Jest directamente (esto es técnicamente lo que jest-cli señala para nosotros a través de jest pero debido al requisito de la bandera, tenemos que ir directos).

El test:watch El comando es casi idéntico. La única diferencia es que al final, necesitamos agregar el --watch marca que le dice a Jest que siga ejecutándose y observando los cambios después de su ejecución inicial.

/src/index.test.js

import calculator from './index';

describe('index.js', () => {
  test('calculator.add adds two numbers together', () => {
    const result = calculator.add(19, 88);
    expect(result).toEqual(107);
  });
});

Cuando se trata de escribir nuestras pruebas, Jest ejecutará automáticamente cualquier prueba ubicada dentro de un *.test.js archivo donde * puede ser cualquier nombre que deseemos. Arriba, estamos nombrando nuestro archivo de prueba para que coincida con el archivo donde se encuentra el código de nuestro paquete:index.test.js . La idea aquí es que queremos mantener nuestro código de prueba junto al código real para el que está diseñado.

Eso puede sonar confuso, pero considere lo que estamos haciendo:estamos tratando de simular un usuario del mundo real llamando a nuestro código desde su aplicación. Esto es lo que son las pruebas en la programación. Las pruebas en sí mismas son solo los medios que usamos para automatizar el proceso (por ejemplo, en lugar de tener una hoja de cálculo de pasos manuales que seguiríamos y realizaríamos a mano).

Arriba, nuestro archivo de prueba consta de dos partes principales:un conjunto y una o más pruebas . En las pruebas, un "conjunto" representa un grupo de pruebas relacionadas. Aquí, estamos definiendo un conjunto único para describir nuestro index.js archivo usando el describe() función en Jest. Esa función toma dos argumentos:el nombre de la suite como una cadena (solo estamos usando el nombre del archivo que estamos probando) y una función para llamar dentro de la cual se definen nuestras pruebas.

Una prueba sigue una configuración similar. Toma una descripción de la prueba como una cadena para su primer argumento y luego una función que se llama para ejecutar la prueba.

Centrándose en el test() función que tenemos aquí, como ejemplo, hemos agregado una prueba que asegura que nuestro calculator.add() El método funciona según lo previsto y suma dos números para producir la suma correcta. Para escribir la prueba real (conocida en la jerga de prueba como "ejecución"), llamamos a nuestro calculator.add() función pasando dos números y almacenando la suma en la variable result . A continuación, verificamos que la función devolvió el valor que esperamos.

Aquí, esperamos result igual a 107 que es la suma que esperaríamos obtener si nuestra función se comporta correctamente. En Jest (y en cualquier biblioteca de prueba), podemos agregar múltiples aserciones a una prueba si lo deseamos. Nuevamente, al igual que el código real en nuestro paquete, el qué/cuándo/cómo/por qué de esto cambiará según la intención de su código.

Agreguemos otra prueba para verificar el mal o infeliz ruta para nuestro calculator.add() función:

/src/index.test.js

import calculator from './index';

describe('index.js', () => {
  test('calculator.add throws an error when passed arguments are not numbers', () => {
    expect(() => {
      calculator.add('a', 'b');
    }).toThrow('[calculator.add] Passed arguments must be a number (integer or float).');
  });

  test('calculator.add adds two numbers together', () => {
    const result = calculator.add(19, 88);
    expect(result).toEqual(107);
  });
});

Ligeramente diferente aquí. Recuerde que anteriormente en nuestro código de paquete, agregamos una verificación para asegurarnos de que los valores pasados ​​a cada una de nuestras funciones de calculadora fueran números pasados ​​como argumentos (arrojando un error si no). Aquí, queremos probar que realmente se genera un error cuando un usuario pasa los datos incorrectos.

¡Esto es importante! Una vez más, cuando estamos escribiendo código que otros consumirán en su propio proyecto, queremos estar lo más seguros posible de que nuestro código hará lo que esperamos (y lo que les decimos a otros desarrolladores que esperamos) que haga.

Aquí, porque queremos verificar que nuestra función de calculadora arroja un error, pasamos una función a nuestro expect() y llame a nuestra función desde dentro de eso función, pasándole malos argumentos. Como dice la prueba, esperamos calculator.add() para arrojar un error si los argumentos que se le pasan no son números. Aquí, debido a que estamos pasando dos cadenas, esperamos que la función throw que la función pasó a expect() "atrapará" y usará para evaluar si la afirmación es verdadera usando el .toThrow() método de aserción.

Esa es la esencia de escribir nuestras pruebas. Echemos un vistazo al archivo de prueba completo (las convenciones idénticas se repiten para cada función de calculadora individual).

/src/index.test.js

import calculator from './index';

describe('index.js', () => {
  test('calculator.add throws an error when passed argumen ts are not numbers', () => {
    expect(() => {
      calculator.add('a', 'b');
    }).toThrow('[calculator.add] Passed arguments must be a number (integer or float).');
  });

  test('calculator.subtract throws an error when passed arguments are not numbers', () => {
    expect(() => {
      calculator.subtract('a', 'b');
    }).toThrow('[calculator.subtract] Passed arguments must be a number (integer or float).');
  });

  test('calculator.multiply throws an error when passed arguments are not numbers', () => {
    expect(() => {
      calculator.multiply('a', 'b');
    }).toThrow('[calculator.multiply] Passed arguments must be a number (integer or float).');
  });

  test('calculator.divide throws an error when passed arguments are not numbers', () => {
    expect(() => {
      calculator.divide('a', 'b');
    }).toThrow('[calculator.divide] Passed arguments must be a number (integer or float).');
  });

  test('calculator.add adds two numbers together', () => {
    const result = calculator.add(19, 88);
    expect(result).toEqual(107);
  });

  test('calculator.subtract subtracts two numbers', () => {
    const result = calculator.subtract(128, 51);
    expect(result).toEqual(77);
  });

  test('calculator.multiply multiplies two numbers', () => {
    const result = calculator.multiply(15, 4);
    expect(result).toEqual(60);
  });

  test('calculator.divide divides two numbers', () => {
    const result = calculator.divide(20, 4);
    expect(result).toEqual(5);
  });
});

Para cada función de calculadora, hemos repetido el mismo patrón:verifique que se produzca un error si los argumentos pasados ​​no son números y espere que la función devuelva el resultado correcto según el método previsto (sumar, restar, multiplicar o dividir) .

Si ejecutamos esto en Jest, deberíamos ver nuestras pruebas ejecutadas (y aprobadas):

Eso es todo para nuestras pruebas y el código del paquete. Ahora estamos listos para pasar a las fases finales de preparación de nuestro paquete para su lanzamiento.

Construyendo nuestro código

Si bien técnicamente podríamos lanzar este código ahora, queremos tener en cuenta dos cosas:si el propio proyecto de un desarrollador admitirá o no nuestro código de paquete y el tamaño del código.

En términos generales, es bueno usar una herramienta de compilación para su código para ayudar con estos problemas. Para nuestro paquete, usaremos el esbuild paquete:una herramienta de compilación simple e increíblemente rápida para JavaScript escrita en Go. Para empezar, agréguemoslo a nuestro proyecto como una dependencia:

Terminal

npm install -D esbuild

Nuevamente, como aprendimos anteriormente con Jest, solo necesitaremos esbuild en desarrollo por lo que usamos el npm install -D comando para instalar el paquete en nuestro devDependencies .

/paquete.json

{
  "type": "module",
  "name": "@cheatcodetuts/calculator",
  "version": "0.0.0",
  "description": "",
  "main": "./dist/index.js",
  "scripts": {
    "build": "./node_modules/.bin/esbuild ./src/index.js --format=esm --bundle --outfile=./dist/index.js --platform=node --target=node16.3 --minify",
    "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
    "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "esbuild": "^0.14.1",
    "jest": "^27.4.3",
    "jest-cli": "^27.4.3",
    "semver": "^7.3.5"
  }
}

Similar a lo que hicimos para Jest arriba, de vuelta en nuestro package.json archivo queremos agregar otro script, esta vez llamado build . Este script se encargará de llamar a esbuild para generar la copia construida de nuestro código de paquete.

./node_modules/.bin/esbuild ./src/index.js --format=esm --bundle --outfile=./dist/index.js --platform=node --target=node16.3 --minify

Para llamar al esbuild , nuevamente, similar a cómo ejecutamos Jest, comenzamos nuestro script con ./node_modules/.bin/esbuild . Aquí, el ./ al principio es una forma abreviada de decir "ejecutar el script en esta ruta" y asume que el archivo en esa ruta contiene un script de shell (observe que estamos importando esto desde el .bin carpeta a través de node_modules con el esbuild script para que se instalen automáticamente como parte de npm install -D esbuild ).

Cuando llamamos a esa función, como primer argumento le pasamos la ruta al archivo que queremos que genere, en este caso:./src/index.js . A continuación, usamos algunas banderas opcionales para decirle a esbuild cómo realizar la compilación y dónde almacenar su salida. Queremos hacer lo siguiente:

  • Use el --format=esm marca para garantizar que nuestro código se construya utilizando la sintaxis de ESM.
  • Use el --bundle bandera para decirle a esbuild para agrupar cualquier JavaScript externo en el archivo de salida (no es necesario para nosotros, ya que no tenemos dependencias de terceros en este paquete, pero es bueno saberlo por su cuenta).
  • Use el --outfile=./dist/index.js marca para almacenar la compilación final en el dist carpeta que creamos anteriormente (usando el mismo nombre de archivo que usamos para nuestro código de paquete).
  • Establezca el --platform=node marcar a node de modo que esbuild sabe cómo tratar correctamente las dependencias integradas de Node.js.
  • Establezca el --target=16.3 flag a la versión de Node.js a la que queremos apuntar nuestra compilación. Esta es la versión de Node.js que se ejecuta en mi máquina mientras escribo este tutorial, pero puede ajustar según sea necesario según los requisitos de su propio paquete.
  • Use el --minify bandera para decirle a esbuild para minimizar el código que genera.

Ese último --minify simplificará nuestro código y lo comprimirá a la versión más pequeña posible para garantizar que nuestro paquete sea lo más ligero posible.

Eso es todo lo que tenemos que hacer. Verifique que su secuencia de comandos sea correcta y luego en su terminal (desde la raíz de la carpeta de su paquete) ejecute:

Terminal

npm run build

Después de unos milisegundos (esbuild es increíblemente rápido), debería ver un mensaje de que la construcción está completa y si mira en el /dist carpeta, debería ver un nuevo index.js archivo que contiene la versión compilada y minimizada de nuestro código de paquete (esto no será legible por humanos).

Muy rápido antes de llamar a este paso "hecho", necesitamos actualizar nuestro package.json de main para asegurarse de que NPM dirija a los desarrolladores a la versión correcta de nuestro código cuando lo importen a sus propios proyectos:

/paquete.json

{
  "type": "module",
  "name": "@cheatcodetuts/calculator",
  "version": "0.0.0",
  "description": "",
  "main": "./dist/index.js",
  "scripts": {
    "build": "./node_modules/.bin/esbuild ./src/index.js --format=esm --bundle --outfile=./dist/index.js --platform=node --target=node16.3 --minify",
    "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
    "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "esbuild": "^0.14.1",
    "jest": "^27.4.3",
    "jest-cli": "^27.4.3",
    "semver": "^7.3.5"
  }
}

Aquí, la parte a la que queremos prestar atención es el "main": "./dist/index.js" . Esto asegura que cuando se instala nuestro paquete, el código que se ejecuta es el código ubicado en la ruta especificada aquí. Queremos que este sea nuestro construido copiar (a través de esbuild ) y no nuestro código fuente ya que, como insinuamos anteriormente, la copia compilada es más pequeña y es más probable que sea compatible con la aplicación del desarrollador.

Escribir un guión de lanzamiento

Para el tramo final, ahora, queremos hacer que nuestro trabajo a largo plazo en nuestro paquete sea un poco más fácil. Técnicamente hablando, podemos lanzar nuestro paquete a través de NPM simplemente usando npm publish . Si bien esto funciona, crea un problema:no tenemos forma de probar nuestro paquete localmente. Sí, podemos probar el código a través de nuestras pruebas automatizadas en Jest, pero siempre es bueno verificar que nuestro paquete funcionará según lo previsto cuando se consume en la aplicación de otro desarrollador (nuevamente:este proceso tiene que ver con aumentar la confianza en que nuestro código funciona según lo previsto) .

Desafortunadamente, NPM en sí no ofrece una opción de prueba local. Si bien podemos instalar un paquete localmente en nuestra máquina a través de NPM, el proceso es un poco complicado y agrega confusión que puede generar errores.

En la siguiente sección, aprenderemos sobre una herramienta llamada Verdaccio (vur-dah-chee-oh) que nos ayuda a ejecutar un servidor NPM simulado en nuestra computadora en el que podemos "publicar de forma ficticia" nuestro paquete (sin liberar nuestro código al público).

En preparación para eso, ahora vamos a escribir un script de lanzamiento para nuestro paquete. Este script de lanzamiento nos permitirá dinámicamente...

  1. Versionar nuestro paquete, actualizando nuestro package.json de version campo.
  2. Lanzamiento condicional de nuestro paquete a nuestro servidor Verdaccio, o a NPM para su lanzamiento público.
  3. Evite que el número de versión de nuestro paquete público no esté sincronizado con nuestro número de versión de desarrollo.

Para empezar, el #3 es una pista. Queremos abrir nuestro package.json archivo una vez más y agregue un nuevo campo:developmentVersion , estableciéndolo en 0.0.0 .

/paquete.json

{
  "type": "module",
  "name": "@cheatcodetuts/calculator",
  "version": "0.0.0",
  "developmentVersion": "0.0.0",
  "description": "",
  "main": "./dist/index.js",
  "scripts": {
    "build": "./node_modules/.bin/esbuild ./src/index.js --format=esm --bundle --outfile=./dist/index.js --platform=node --target=node16.3 --minify",
    "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
    "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "esbuild": "^0.14.1",
    "jest": "^27.4.3",
    "jest-cli": "^27.4.3"
  }
}

Cerca de la parte superior de nuestro archivo, justo debajo del version campo, hemos añadido developmentVersion y establézcalo en 0.0.0 . Es importante tener en cuenta que la versión de desarrollo es un campo no estándar en un archivo package.json . Este campo es solo para nosotros y NPM no lo reconoce.

Nuestro objetivo con este campo, como veremos a continuación, es tener una versión de nuestro paquete que sea independiente de la versión de producción. Esto se debe a que cada vez que lanzamos nuestro paquete (localmente o en producción/público), NPM intentará crear una versión de nuestro paquete. Como es probable que tengamos varias versiones de desarrollo, queremos evitar saltar versiones de producción de algo como 0.1.0 a 0.50.0 donde los 49 lanzamientos entre los dos son solo para nosotros probando nuestra versión de desarrollo del paquete (y no reflejan los cambios reales en el paquete principal).

Para evitar ese escenario, nuestro script de lanzamiento negociará entre estas dos versiones según el valor de process.env.NODE_ENV y mantener nuestras versiones ordenadas.

/release.js

import { execSync } from "child_process";
import semver from "semver";
import fs from 'fs';

const getPackageJSON = () => {
  const packageJSON = fs.readFileSync('./package.json', 'utf-8');
  return JSON.parse(packageJSON);
};

const setPackageJSONVersions = (originalVersion, version) => {
  packageJSON.version = originalVersion;
  packageJSON.developmentVersion = version;
  fs.writeFileSync('package.json', JSON.stringify(packageJSON, null, 2));
};

const packageJSON = getPackageJSON();
const originalVersion = `${packageJSON.version}`;
const version = semver.inc(
  process.env.NODE_ENV === 'development' ? packageJSON.developmentVersion : packageJSON.version,
  'minor'
);

const force = process.env.NODE_ENV === "development" ? "--force" : "";

const registry =
  process.env.NODE_ENV === "development"
    ? "--registry http://localhost:4873"
    : "";

try {
  execSync(
    `npm version ${version} --allow-same-version ${registry} && npm publish --access public ${force} ${registry}`
  );
} catch (exception) {
  setPackageJSONVersions(originalVersion, version);
}

if (process.env.NODE_ENV === 'development') {
  setPackageJSONVersions(originalVersion, version);
}

Esta es la totalidad de nuestro guión de lanzamiento. Muy rápido, en la parte superior notará una dependencia adicional que necesitamos agregar semver :

Terminal

npm install -D semver

Centrándonos en la mitad de nuestro código de script de lanzamiento, lo primero que debemos hacer es obtener el contenido actual de nuestro package.json archivo cargado en la memoria. Para hacer esto, cerca de la parte superior de nuestro archivo, hemos agregado una función getPackageJSON() que lee el contenido de nuestro archivo en la memoria como una cadena usando fs.readFileSync() y luego analiza esa cadena en un objeto JSON usando JSON.parse() .

A continuación, con nuestro package.json archivo cargado en la variable packageJSON , almacenamos o "copiamos" el originalVersion , asegurándose de almacenar el valor dentro de una cadena usando acentos graves (esto entrará en juego cuando volvamos a configurar dinámicamente la versión en nuestro package.json archivo más adelante en el script).

Después de esto, usando el semver paquete que acabamos de instalar, queremos incrementar la versión de nuestro paquete. Aquí, semver es la abreviatura de versión semántica, que es un estándar ampliamente aceptado para escribir versiones de software. El semver El paquete que estamos usando aquí nos ayuda a generar números de versión semántica (como 0.1.0 o 1.3.9 ) y analícelos para su evaluación en nuestro código.

Aquí, semver.inc() está diseñado para incrementar la versión semántica que pasamos como primer argumento, incrementándola en función de la "regla" que pasamos como segundo argumento. Aquí, estamos diciendo "if process.env.NODE_ENV es desarrollo, queremos incrementar el developmentVersion de nuestro package.json y si no, queremos incrementar el version normal campo de nuestro package.json ."

Para el segundo argumento aquí, estamos usando el minor regla que le dice a semver para incrementar nuestra versión en función del número del medio en nuestro código. Eso está claro, una versión semántica tiene tres números:

major.minor.patch

De forma predeterminada, configuramos nuestro developmentVersion y version a 0.0.0 por lo tanto, la primera vez que ejecutamos una versión, esperamos que este número se incremente a 0.1.0 y luego 0.2.0 y así sucesivamente.

Con nuestra nueva versión almacenada en el version variable, a continuación, debemos tomar dos decisiones más, ambas basadas en el valor de process.env.NODE_ENV . La primera es decidir si queremos forzar la publicación de nuestro paquete (esto obligará a que se publique la versión) y el segundo decide en qué registro queremos publicar (nuestro servidor Verdaccio, o bien, en el registro principal de NPM). Para el registry variable, anticipamos que Verdaccio se ejecutará en su puerto predeterminado en localhost, por lo que configuramos el --registry marca a http://localhost:4873 donde 4873 es el puerto predeterminado de Verdaccio.

Porque incorporaremos estas variables force y registry en un comando a continuación, si son no requerido, simplemente devolvemos una cadena vacía (que es similar a un valor vacío/sin configuración).

/release.js

try {
  execSync(
    `npm version ${version} --allow-same-version ${registry} && npm publish --access public ${force} ${registry}`
  );
} catch (exception) {
  setPackageJSONVersions(originalVersion, version);
}

if (process.env.NODE_ENV === 'development') {
  setPackageJSONVersions(originalVersion, version);
}

Ahora viene la parte divertida. Para crear un lanzamiento, necesitamos ejecutar dos comandos:npm version y npm publish . Aquí, npm version es responsable de actualizar la versión de nuestro paquete dentro de package.json y npm publish realiza la publicación real del paquete.

Para el npm version paso, observe que estamos pasando el version incrementado generamos usando semver.inc() arriba, así como el registry variable que determinamos justo antes de esta línea. Esto le dice a NPM que establezca la versión en la pasada como version y para asegurarse de ejecutar esta versión contra el registry apropiado .

A continuación, para la publicación real, llamamos al npm publish comando pasando el --access marcar como public junto con nuestro force y registry banderas Aquí, el --access public asegura que los paquetes que usan un ámbito name se hacen accesibles al público (por defecto, este tipo de paquetes se hacen privados).

Un paquete con ámbito es aquel cuyo nombre se parece a @username/package-name donde el @username parte es el "alcance". Un paquete sin ámbito, por el contrario, es solo package-name .

Para ejecutar este comando, observe que estamos usando el execSync() función importada de Node.js child_process paquete (esto está integrado en Node.js y no es algo que necesitemos instalar por separado).

Si bien esto técnicamente se ocupa de nuestro lanzamiento, hay dos líneas más para llamar. Primero, observe que hemos ejecutado nuestro execSync() llame a un try/catch bloquear. Esto se debe a que debemos anticiparnos a posibles fallas en la publicación de nuestro paquete. Más específicamente, queremos asegurarnos de no dejar accidentalmente una nueva versión que aún no se haya publicado (debido a una falla en el script) en nuestro package.json archivo.

Para ayudar a administrar esto, hemos agregado una función en la parte superior llamada setPackageJSONVersions() que toma el originalVersion y nuevo version creamos anteriormente en el script. Llamamos a esto en el catch bloque de nuestro código aquí para asegurarnos de que las versiones se mantengan limpias en caso de falla.

/release.js

const setPackageJSONVersions = (originalVersion, version) => {
  packageJSON.version = originalVersion;
  packageJSON.developmentVersion = version;
  fs.writeFileSync('package.json', JSON.stringify(packageJSON, null, 2));
};

Esta función toma el packageJSON valor que recuperamos anteriormente y almacenamos en esa variable y modifica su version y developmentVersion campos. Si miramos de cerca, nos aseguramos de establecer el version campo de vuelta al originalVersion y el developmentVersion al nuevo version .

Esto es intencional. Cuando ejecutamos npm version en el comando que pasamos a execSync() , pase lo que pase, NPM intentará incrementar el version campo en nuestro package.json expediente. Esto es problemático ya que solo queremos hacer esto cuando intentamos realizar un real lanzamiento de la producción. Este código mitiga este problema al escribir sobre cualquier cambio que realice NPM (lo que consideraríamos como un accidente), asegurando que nuestras versiones permanezcan sincronizadas.

Si miramos hacia abajo en nuestro script de lanzamiento, justo en la parte inferior, hacemos una llamada a esta función nuevamente si process.env.NODE_ENV === 'development' , con la intención de sobrescribir el version modificado campo a la versión original/actual y actualice el developmentVersion a la nueva versión.

¡Casi termino! Ahora, con nuestro script de lanzamiento listo, necesitamos hacer una última adición a nuestro package.json archivo:

/paquete.json

{
  "type": "module",
  "name": "@cheatcodetuts/calculator",
  "version": "0.4.0",
  "developmentVersion": "0.7.0",
  "description": "",
  "main": "./dist/index.js",
  "scripts": {
    "build": "./node_modules/.bin/esbuild ./src/index.js --format=esm --bundle --outfile=./dist/index.js --platform=node --target=node16.3 --minify",
    "release:development": "export NODE_ENV=development && npm run build && node ./release.js",
    "release:production": "export NODE_ENV=production && npm run build && node ./release.js",
    "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
    "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "esbuild": "^0.14.1",
    "jest": "^27.4.3",
    "jest-cli": "^27.4.3",
    "semver": "^7.3.5"
  }
}

Aquí, queremos agregar dos nuevos scripts :release:development y release:production . Los nombres deberían ser bastante obvios aquí. Un script está destinado a lanzar una nueva versión de nuestro paquete en desarrollo (para Verdaccio), mientras que el otro está destinado a publicarse en el registro principal de NPM.

El guión consta de tres partes:

  1. Primero, se asegura de establecer el valor apropiado para process.env.NODE_ENV (ya sea development o production ).
  2. Ejecuta una compilación nueva de nuestro paquete a través de npm run build llamando a nuestro build guión anterior.
  3. Ejecuta nuestro script de lanzamiento usando node ./release.js .

Eso es todo. Ahora, cuando ejecutamos npm run release:development o npm run release:production , configuraremos el entorno adecuado, crearemos nuestro código y lanzaremos nuestro paquete.

Pruebas locales con Verdaccio y Joystick

Ahora, para probar todo esto, estamos finalmente va a configurar Verdaccio localmente. La buena noticia:solo tenemos que instalar un paquete y luego iniciar el servidor; eso es todo.

Terminal

npm install -g verdaccio

Aquí, estamos usando npm install pero observe que estamos usando el -g bandera que significa instalar Verdaccio globalmente en nuestra computadora, no solo dentro de nuestro proyecto (intencionalmente, ya que queremos poder ejecutar Verdaccio desde cualquier lugar).

Terminal

verdaccio

Una vez instalado, para ejecutarlo basta con teclear verdaccio en nuestra terminal y ejecutarlo. Después de unos segundos, debería ver un resultado como este:

$ verdaccio
warn --- config file  - /Users/rglover/.config/verdaccio/config.yaml
warn --- Plugin successfully loaded: verdaccio-htpasswd
warn --- Plugin successfully loaded: verdaccio-audit
warn --- http address - http://localhost:4873/ - verdaccio/5.2.0

Con eso en ejecución, ahora podemos ejecutar una versión de prueba de nuestro paquete. De vuelta en la raíz de la carpeta del paquete, intentemos ejecutar esto:

Terminal

npm run release:development

Si todo va bien, debería ver un resultado similar a este (su número de versión será 0.1.0 :

> @cheatcodetuts/[email protected] build
> ./node_modules/.bin/esbuild ./src/index.js --format=esm --bundle --outfile=./dist/index.js --platform=node --target=node16.3 --minify

  dist/index.js  600b

⚡ Done in 19ms
npm WARN using --force Recommended protections disabled.
npm notice
npm notice 📦  @cheatcodetuts/[email protected]
npm notice === Tarball Contents ===
npm notice 50B   README.md
npm notice 600B  dist/index.js
npm notice 873B  package.json
npm notice 1.2kB release.js
npm notice 781B  src/index.js
npm notice 1.6kB src/index.test.js
npm notice === Tarball Details ===
npm notice name:          @cheatcodetuts/calculator
npm notice version:       0.8.0
npm notice filename:      @cheatcodetuts/calculator-0.8.0.tgz
npm notice package size:  1.6 kB
npm notice unpacked size: 5.1 kB
npm notice shasum:        87560b899dc68b70c129f9dfd4904b407cb0a635
npm notice integrity:     sha512-VAlFAxkb53kt2[...]EqCULQ77OOt0w==
npm notice total files:   6
npm notice

Ahora, para verificar que nuestro paquete se lanzó a Verdaccio, podemos abrir nuestro navegador en http://localhost:4873 y ver si aparece nuestro paquete:

Si bien es genial que haya funcionado, ahora queremos probar este paquete rápidamente en una aplicación real.

Probando el paquete en desarrollo

Para probar nuestro paquete, vamos a aprovechar el marco Joystick de CheatCode para ayudarnos a activar rápidamente una aplicación con la que podamos probar. Para instalarlo, en su terminal ejecute:

Terminal

npm install -g @joystick.js/cli

Y una vez que esté instalado, desde fuera del directorio de su paquete, ejecute:

Terminal

joystick create package-test

Después de unos segundos, verá un mensaje de Joystick indicándole que cd en package-test y ejecuta joystick start . Antes de ejecutar joystick start instalemos nuestro paquete en la carpeta que se creó para nosotros:

Terminal

cd package-test && npm install @cheatcodetuts/calculator --registry http://localhost:4873

Aquí, cd en nuestra carpeta de aplicaciones de prueba y ejecute npm install especificando el nombre de nuestro paquete seguido de un --registry indicador establecido en la URL de nuestro servidor Verdaccio http://localhost:4873 . Esto le dice a NPM que busque el paquete especificado en esa URL . Si dejamos el --registry parte aquí, NPM intentará instalar el paquete desde su registro principal.

Una vez que su paquete se haya instalado, continúe e inicie Joystick:

Terminal

joystick start

Luego, continúa y abre ese package-test carpeta en un IDE (por ejemplo, VSCode) y luego navegue a index.server.js archivo generado para usted en la raíz de esa carpeta:

/index.servidor.js

import node from "@joystick.js/node";
import calculator from "@cheatcodetuts/calculator";
import api from "./api";

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.status(200).send(`${calculator.divide(51, 5)}`);
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

En la parte superior de ese archivo, queremos importar la exportación predeterminada de nuestro paquete (en el ejemplo, el calculator objeto que pasamos a export default en nuestro código de paquete).

Para probarlo, hemos "secuestrado" el ejemplo / ruta en nuestra aplicación de demostración. Allí, usamos el servidor Express.js integrado en Joystick para decir "devuelve un código de estado de 200 y una cadena que contiene los resultados de llamar a calculator.divide(51, 5) ." Suponiendo que esto funcione, si abrimos nuestro navegador web, deberíamos ver el número 10.2 impreso en el navegador:

¡Impresionante! Si podemos ver esto, eso significa que nuestro paquete está funcionando, ya que pudimos importarlo a nuestra aplicación y llamar a su funcionalidad sin ningún problema (obteniendo el resultado deseado).

Liberación a producción

Bueno. Hora del gran final. Con todo eso completo, finalmente estamos listos para publicar nuestro paquete al público a través de NPM. Muy rápido, asegúrese de haber configurado una cuenta en NPM y haber iniciado sesión en esa cuenta en su computadora usando el npm login método:

Terminal

npm login

Después de eso, las buenas noticias:es solo un comando para hacerlo. Desde la raíz de nuestra carpeta de paquetes:

Terminal

npm run release:production

Idéntico a lo que vimos con nuestra llamada a release:development , deberíamos ver un resultado como este después de unos segundos:

$ npm run release:production

> @cheatcodetuts/[email protected] release:production
> export NODE_ENV=production && npm run build && node ./release.js


> @cheatcodetuts/[email protected] build
> ./node_modules/.bin/esbuild ./src/index.js --format=esm --bundle --outfile=./dist/index.js --platform=node --target=node16.3 --minify

  dist/index.js  600b

⚡ Done in 1ms
npm notice
npm notice 📦  @cheatcodetuts/[email protected]
npm notice === Tarball Contents ===
npm notice 50B   README.md
npm notice 600B  dist/index.js
npm notice 873B  package.json
npm notice 1.2kB release.js
npm notice 781B  src/index.js
npm notice 1.6kB src/index.test.js
npm notice === Tarball Details ===
npm notice name:          @cheatcodetuts/calculator
npm notice version:       0.5.0
npm notice filename:      @cheatcodetuts/calculator-0.5.0.tgz
npm notice package size:  1.6 kB
npm notice unpacked size: 5.1 kB
npm notice shasum:        581fd5027d117b5e8b2591db68359b08317cd0ab
npm notice integrity:     sha512-erjv0/VftzU0t[...]wJoogfLORyHZA==
npm notice total files:   6
npm notice

¡Eso es todo! Si nos dirigimos a NPM, deberíamos ver nuestro paquete publicado (advertencia justa, NPM tiene un caché agresivo, por lo que es posible que deba actualizar varias veces antes de que aparezca):

Todo listo. ¡Felicitaciones!

Terminando

In this tutorial, we learned how to write an NPM package using Node.js and JavaScript. We learned how to write our package code, write tests for it using Jest, and how to build it for a production release using esbuild . Finally, we learned how to write a release script that helped us to publish to both a local package repository (using Verdaccio) and to the main NPM repository.