Cómo crear y publicar un módulo para npm
JavaScript tiene el ecosistema más grande de módulos los cuales se alojan en GitHub y se publican al registro de npm para su posterior uso en proyectos.
Usar un módulo desde npm es bastante sencillo, solo necesitamos correr npm install <module>
o yarn add <module>
(reemplazando <module>
por el nombre del módulo a instalar) y listo, el módulo se va a registrar como dependencia de nuestro proyecto y vamos a poder importarlo en nuestro código.
En este tutorial crearemos un módulo, lo más completo posible, y lo publicaremos al registro de npm para que cualquiera lo use.
Inicia el proyecto
Primero hay que iniciar el proyecto, para esto se puede usar npx, una herramienta que viene junto a npm que permite instalar, temporalmente, y ejecutar módulos desde npm. Creá una carpeta para el proyecto con el nombre del proyecto, digamos my-module
, y dentro ejecutá los siguientes comandos.
git init
echo "# my-module" > README.md
npx license MIT -o "Sergio Xalambrí" > LICENSE
npx gitignore node
npx covgen "hello@sergiodxa.com"
npm init -y # o yarn init -y
git add -A
git commit -m "Initial commit"
Vayamos línea por línea viendo que hace cada comando.
- Se inicia git en la carpeta de nuestro proyecto
- Se crea un archivo README.md con el contenido
# my-module
- Se genera una licencia MIT con nuestro nombre y la guardamos en el archivo LICENSE
- Se genera un
.gitignore
genérico para proyectos de Node.js y JavaScript - Se genera un archivo CONTRIBUTING.md con nuestro email siguiendo el Contributor Covenant
- Se inicia el proyecto de Node.js haciendo que se cree un package.json
- Se agregan todos los archivos a git
- Se crea un commit inicial
Con estos ya está todo listo para empezar el proyecto, y nuestra carpeta debería tener estos archivos.
[
{ "type": "file", "name": ".gitignore" },
{ "type": "file", "name": "package.json" },
{ "type": "file", "name": "README.md" },
{ "type": "file", "name": "LICENSE" }
]
Manifiesto
El manifiest es el archivo package.json
, este posee información del módulo como el nombre, versión, autor y dependencias, entre otras. Con el sexto comando que corriste antes se creó uno inicial que debería verse más o menos así
{
"name": "my-module",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Nota: Depende de tu configuración de npm puede haber más o menos información inicial, este es uno básico.
Vamos a ver que es cada propiedad.
name
es el nombre del módulo, por defecto el nombre de la carpeta, el nombre puede además incluír un namespace en la forma de@namespace/my-module
, el namespace debe ser o tu nombre de usuario en npm o una organización a la que pertenezcas.version
es la versión del módulo, siguiente el sistema de versionamiento semático (semver).description
es la descripción del proyecto, se usa en el registro de npm para mostrar que hace el proyecto, lo ideal es que sea corta y concisa.main
es la ubicación u nombre del archivo principal de nuestro código, por defecto esindex.js
, en caso de que decidamos usar otro archivo diferente o con otro nombre vamos a necesitarlo.scripts
es un objeto que posee listas de scripts con nombres que podemos luego ejecutar haciendonpm run <script>
oyarn <script>
, por defecto viene un scripttest
que muestra une error (nota:test
es uno de varios scripts especiales que se pueden ejecutar connpm <script>
sin el run).keywords
son palabras claves que puede usar el registro para que nuestro módulo salga en ciertas búsquedasauthor
es la información del autor, puede ser un string con el formatoname <url> (email)
o un objeto con esas propiedadeslicense
es la licencia bajo la cual publicas el módulo, en nuestro caso vamos a usar MIT por lo que deberíamos cambiarlo.
El código
Creá una carpeta src
con el archivo index.js
que tenga esto dentro:
function hello(name = "Sergio") {
return `Hello, ${name}`;
}
export default hello;
Algo super simple, podés poner cualquier contenido en realidad, pero para el ejemplo voy a usar ese. Nuestro proyecto ahora debería verse así.
[
{
"type": "folder",
"name": "src",
"children": [
{ "type": "file", "name": "index.js" }
]
},
{ "type": "file", "name": ".gitignore" },
{ "type": "file", "name": "package.json" },
{ "type": "file", "name": "README.md" },
{ "type": "file", "name": "LICENSE" }
]
Dependencias
El código de este módulo es super simple y no necesita dependencias, en caso de que las necesite se pueden instalar haciendo
npm install another-module
# yarn add another-module
También es importante saber que existen varios tipos de dependencias que en el package.json
se definen con distintos nombres.
dependencies
son las dependencias del código las cuales se usan directamente haciendoimport
en el código del módulodevDependencies
se usan solo en desarrollo, normalmente son herramientas que se usan al desarrollar el módulo, como pueden ser frameworks de pruebas o herramientas de compilaciónpeerDependencies
son dependencias que se usan directo en el código, pero que se espera que el usuario del módulo provea, esto es normal para que se pueda usar cualquier versión de estas dependencias.
Hay más, pero estas son las más comunes, en el caso de my-module
no hay dependencies
pero si va a haber devDependencies
, para instalarlas se usa el comando
npm install -D another-module
# yarn add -D another-module
Donde la -D
indica que es una dependencia de desarrollo. Las que se van a usar en el módulo, y que yo recomiendo son dos.
El primero es un framework de pruebas creado por Facebook que va a servir para automatizar las pruebas del código.
El segundo es una herramienta del creador de Preact, y otro muchos módulos pequeños, que sirve para hacer distintas versiones del código compatibles con varios sistemas de módulos que existen, en total soporta 3, un módulo de CommonJS para usar en Node.js, uno de UMD para usar en una etiqueta script en el navegador y uno de ECMAScript que usa export
e import
para que herramientas como webpack o Parcel puedan optimizar el código final.
Para instalar ambos el comando final sería
npm install -D jest microbundle
# yarn add -D jest microbundle
Nota: como se ve, se pueden poner varios módulos separados por espacios
Configurando los scripts
Ya teniendo instaladas las dependencias hay que configurar scripts en el package.json
para usarlas. Si abrís el archivo debería estar actualizado para verse así.
{
"name": "@sergiodxa/my-module",
"version": "1.0.0",
"description": "My super cool module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Sergio Xalambrí <hello@sergiodxa.com> (https://sergiodxa.com)",
"license": "MIT",
"devDependencies": {
"jest": "^24.1.0",
"microbundle": "^0.9.0"
}
}
Ahora el package.json
tiene la propiedad devDependencies
, además ya deberías haber cambiando license
por MIT, poner tu nombre, email y sitio web en author
y una descripción. El nombre del módulo en este caso como ya está usado es necesario ponerle un namespace.
Ya visto eso, para configurar los scripts solo hay que actualizar el objeto scripts
con el siguiente.
{
"test": "jest",
"build": "microbundle"
}
El primero es para ejecutar las pruebas y el segundo para hacer "build" del módulo usanod microbundle. el proyecto debería ahora verse así (el yarn.lock
solo aparece si usaste yarn para instalar módulo, si usas npm habría un package-lock.json
).
[
{
"type": "folder",
"name": "src",
"children": [
{ "type": "file", "name": "index.js" }
]
},
{ "type": "file", "name": ".gitignore" },
{ "type": "file", "name": "package.json" },
{ "type": "file", "name": "README.md" },
{ "type": "file", "name": "LICENSE" },
{ "type": "file", "name": "yarn.lock" }
]
Agregando pruebas
Idealmente un módulo siempre debería tener pruebas automatizadas, en este caso las pruebas que se pueden agregar son bastantes simples, usar la función hello
con y sin valor de name
. Para esto creamos un archivo src/index.test.js
con el archivo de del test.
mport hello from ".";
describe("it should say hello", () => {
it("should greet 'Sergio'", () => {
expect(hello()).toBe("Hello, Sergio");
});
it("should greet 'Daniel'", () => {
expect(hello("Daniel")).toBe("Hello, Daniel");
});
});
Hasta ahora nuestro proyecto debería verse así en el sistema de archivos
[
{
"type": "folder",
"name": "src",
"children": [
{ "type": "file", "name": "index.js" },
{ "type": "file", "name": "index.test.js" }
]
},
{ "type": "file", "name": ".gitignore" },
{ "type": "file", "name": "package.json" },
{ "type": "file", "name": "README.md" },
{ "type": "file", "name": "LICENSE" },
{ "type": "file", "name": "yarn.lock" }
]
Soportando ESModules en Jest
Como usamos módulo de ECMAScript en nuestro código y Jest no lo soporta (debido a que Node.js no lo soporta) entonces necesitamos usar una herramienta para dar soporte.
Para estos usamos babel-jest
y @babel/preset-env
.
npm install -D babel-jest @babel/preset-env
# yarn add -D babel-jest @babel/preset-env
Ahora creamos un babel.config.js
en la raíz del proyecto y colocamos el siguiente código.
module.exports = {
presets: ["@babel/preset-env"]
};
El proyecto debería verse ahora de esta forma.
[
{
"type": "folder",
"name": "src",
"children": [
{ "type": "file", "name": "index.js" },
{ "type": "file", "name": "index.test.js" }
]
},
{ "type": "file", "name": ".gitignore" },
{ "type": "file", "name": "babel.config.js" },
{ "type": "file", "name": "package.json" },
{ "type": "file", "name": "README.md" },
{ "type": "file", "name": "LICENSE" },
{ "type": "file", "name": "yarn.lock" }
]
Corriendo las pruebas
Con todo listo, ya podemos ver si nuestro módulo funciona, para esto corremos las pruebas con yarn test
y deberíamos ver algo así.
$ yarn test
yarn run v1.13.0
$ jest
PASS src/index.test.js
it should say hello
✓ should greet 'Sergio' (4ms)
✓ should greet 'Daniel'
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 2.1s
Ran all test suites.
✨ Done in 3.35s.
¡Si vemos esto es que todo pasó y el código funciona!
Construyendo archivos para producción
Ahora es necesario construir los archivos para producción de nuestro módulo, los que vamos a publicar a npm. Esto lo hacemos con la dependencia de desarrollo microbundle que instalamos antes y lo ejecutamos con yarn build
.
Por defecto microbundle va a colocar todos los archivos en la raíz del proyecto, vamos a configurarlo para que queden en una carpeta dist
que podemos agregar al archivo .gitignore
, para configurar microbundle, para configurarlo usamos el package.json
.
{
"name": "@sergiodxa/my-module",
"version": "1.0.0",
"description": "My super cool module",
"main": "dist/index.js",
"umd:main": "dist/index.umd.js",
"module": "dist/index.mjs",
"source": "src/index.js",
"scripts": {
"test": "jest",
"build": "microbundle"
},
"keywords": [],
"author": "Sergio Xalambrí <hello@sergiodxa.com> (https://sergiodxa.com)",
"license": "MIT",
"dependencies": {},
"devDependencies": {
"@babel/preset-env": "^7.3.4",
"babel-jest": "^24.4.0",
"jest": "^24.1.0",
"microbundle": "^0.9.0"
}
}
Agregando main
con la ruta del archivo para Node.js (CJS), umd:main
para el archivo UMD y module
para el archivo de ESM le estamos diciendo a microbundle que los archivos que genere deben estar en esas ubicaciones. La propiedad source
indica el archivo fuente que va a usar microbundle para empezar a construir nuestro módulo.
Si ahora ejecutamos npm run build
o yarn build
va a generar la carpeta dist
con el código.
[
{
"close": true,
"type": "folder",
"name": "dist",
"children": [
{ "type": "file", "name": "index.js" },
{ "type": "file", "name": "index.js.map" },
{ "type": "file", "name": "index.mjs" },
{ "type": "file", "name": "index.mjs.map" },
{ "type": "file", "name": "index.umd.js" },
{ "type": "file", "name": "index.umd.js.map" }
]
},
{
"type": "folder",
"name": "src",
"children": [
{ "type": "file", "name": "index.js" },
{ "type": "file", "name": "index.test.js" }
]
},
{ "type": "file", "name": ".gitignore" },
{ "type": "file", "name": "babel.config.js" },
{ "type": "file", "name": "package.json" },
{ "type": "file", "name": "README.md" },
{ "type": "file", "name": "LICENSE" },
{ "type": "file", "name": "yarn.lock" }
]
Configurando prepublish
Algo que puede pasar es que nos olvidemos de hacer build
antes de publicar nuestro módulo, para esto podemos definir un hook en nuestro scripts del package.json
, para esto npm nos ofrece poner un script con el prefijo pre
, este script va a correr antes de ejecutar otro script prefijado. Mejor veamos con un ejemplo, si hay un script build
podemos tener prebuild
para ejecutar otro script antes de hacer build
.
En el caso de publish
que es usado para publicar un módulo podemos poner prepublish
donde vamos a ejecutar npm run build
para hacer build del proyecto.
{
"name": "@sergiodxa/my-module",
"version": "1.0.0",
"description": "My super cool module",
"main": "dist/index.js",
"umd:main": "dist/index.umd.js",
"module": "dist/index.mjs",
"source": "src/index.js",
"scripts": {
"test": "jest",
"build": "microbundle",
"prepublish": "npm run build"
},
"keywords": [],
"author": "Sergio Xalambrí <hello@sergiodxa.com> (https://sergiodxa.com)",
"license": "MIT",
"dependencies": {},
"devDependencies": {
"@babel/preset-env": "^7.3.4",
"babel-jest": "^24.4.0",
"jest": "^24.1.0",
"microbundle": "^0.9.0"
}
}
Con esto, cuando vayamos a publicar nuestro módulo vamos a estar seguros de que antes se hizo build. Adicionalmente podemos agregar un prebuild
script para ejecutar npm test
y nunca hacer build si las pruebas no pasan primero.
Evitándose commitear archivos con errores
Otra cosa que podemos hacer es asegurarnos de nunca commitear a nuestro repositorio código que no pase las pruebas o que no pase un linter para ver que esté bien escrito. Para esto existen unas herramientas llamadas husky y lint-staged que nos sirven para ejecutar un script antes de hacer commit.
Para usarlas primero necesitamos instalarlas.
npm install -D husky lint-staged
# yarn add -D husky lint-staged
Después a nuestro package.json
le agregamos la configuración de estas herramientas.
{
"husky": {
"hooks": {
"pre-commit": "npm test"
}
}
}
Con esa configuración vamos a ejecutar las pruebas de nuestro módulo antes de hacer commit, si las pruebas no pasan entonces no se permite hacer commit hasta que pasen, con esto nos aseguramos de que nada se guarda en Git si las pruebas están rotas.
También instalamos lint-staged
, esta herramienta nos permite ejecutar algo solo para los archivos actualizados o agregados. Podemos usarlo entonces para usar un linter y asegurarnos de que el código cumpla ciertas reglas.
Configurando un Linter
Para asegurarnos de que nuestro código cumple ciertas reglas podemos usar un Linter, un linter lo que hace es leer nuestro código de forma estática (sin ejecutarlo) y detectar errores según ciertas reglas preconfiguradas.
Vamos a usar ESLint y Prettier para asegurarnos de que nuestro código no tenga errores y esté escrito con un estilo igual sin importar cuantas personas trabajen el código del módulo.
npm install -D eslint eslint-plugin-prettier eslint-config-prettier prettier
# yarn add -D eslint eslint-plugin-prettier eslint-config-prettier prettier
Ahora vamos a configurar ESLint creando un archivo .eslintrc
con el siguiente contenido.
{
"extends": ["prettier"],
"plugins": ["prettier"],
"parserOptions": {
"ecmaVersion": 2018
},
"env": {
"node": true,
"es6": true
}
}
Con esto le decimos a ESLint que extienda la configuración de Prettier, que lo use como Plugin, que soporte ECMAScript 2018 y que el código se ejecuta en entornos ES6 o más y Node.js. Ahora hay que hacer que en cada cambio a un archivo .js
se ejecute prettier y ESLint, para eso configuramos en el package.json
lo siguiente.
{
"husky": {
"hooks": {
"pre-commit": "npm test && lint-staged"
}
},
"lint-staged": {
"*.js": ["prettier --write", "eslint --fix", "git add"],
"*.{json,md}": ["prettier --write", "git add"]
}
}
Ahora Prettier va a arreglar cualquier problema en archivos .json
y .md
y Prettier y ESLint van a ver problemas en archivos .js
.
Agregando tipado
TypeScript es cada vez más popular en la comundidad de JavaScript, FlowType aunque no tan popular tiene sus usuarios, incluso si tu código no usa directamente estos lenguajes es posible proveer los tipos de datos que usa nuestro módulo y ayudar a quienes usan estas herramientas.
Para esto vamos a crear un archivo index.d.ts
y un archivo index.js.flow
con los tipos de datos de nuestro módulo, en este caso es bastante simple.
export default function hello(name: string): string;
Esos son todos los tipos necesarios por nuestro módulo. Este mismo código se puede agregar a ambos archivos. Luego de esto nuestro proyecto debería verse así.
[
{
"close": true,
"type": "folder",
"name": "dist",
"children": [
{ "type": "file", "name": "index.js" },
{ "type": "file", "name": "index.js.map" },
{ "type": "file", "name": "index.mjs" },
{ "type": "file", "name": "index.mjs.map" },
{ "type": "file", "name": "index.umd.js" },
{ "type": "file", "name": "index.umd.js.map" }
]
},
{
"type": "folder",
"name": "src",
"children": [
{ "type": "file", "name": "index.js" },
{ "type": "file", "name": "index.test.js" }
]
},
{ "type": "file", "name": ".gitignore" },
{ "type": "file", "name": "babel.config.js" },
{ "type": "file", "name": "index.d.ts" },
{ "type": "file", "name": "index.js.flow" },
{ "type": "file", "name": "package.json" },
{ "type": "file", "name": "README.md" },
{ "type": "file", "name": "LICENSE" },
{ "type": "file", "name": "yarn.lock" }
]
Publicando
Ya con todo listo y configurado, es hora de publicar el módulo, primero hay que estar seguro de hacer commit de todos los archivos cambiados y hacerles push a nuestro repositorio de Github. Después vamos a definir que archivos vamos a subir a npm creando en nuestro package.json
una key files
con un array de los archivos y carpetas a subir.
{
"name": "@sergiodxa/my-module",
"version": "1.0.0",
"description": "My super cool module",
"main": "dist/index.js",
"umd:main": "dist/index.umd.js",
"module": "dist/index.mjs",
"source": "src/index.js",
"scripts": {
"test": "jest",
"prebuild": "npm test",
"build": "microbundle",
"prepublish": "npm run build"
},
"keywords": [],
"author": "Sergio Xalambrí <hello@sergiodxa.com> (https://sergiodxa.com)",
"license": "MIT",
"dependencies": {},
"devDependencies": {
"@babel/preset-env": "^7.3.4",
"babel-jest": "^24.4.0",
"husky": "^1.3.1",
"jest": "^24.1.0",
"lint-staged": "^8.1.5",
"microbundle": "^0.9.0"
},
"husky": {
"hooks": {
"pre-commit": "npm test && lint-staged"
}
},
"lint-staged": {
"*.js": ["prettier --write", "eslint --fix", "git add"],
"*.{json,md}": ["prettier --write", "git add"]
},
"files": ["dist", "index.d.ts", "index.js.flow", "package.json", "README.md"]
}
¡Ahora sí, hora de publicar¡ Esto lo hacemos con el siguiente comando.
npm publish
Si da un error por usar un namespace diciéndo que pagues para publicar módulos privados hay que agregar --access public
al comando.
npm publish --access public
¡Ya con esto va a estar publicado! Este mismo módulo se puede bajar desde @sergiodxa/my-module.
Palabras finales
Son muchos pasos, pero no todos son realmente necesarios, con tener el package.json
y el código es suficiente para hacer publish
, en ese artículo fuimos un poco más allá agregando un build, pruebas automatizadas, linter y más cosas para asegurarnos de publicar un buen módulo lo más completo.