¿Cómo publicar un módulo escrito en ES6 a NPM?

144

Estaba a punto de publicar un módulo en NPM, cuando pensé en reescribirlo en ES6, para prepararlo en el futuro y aprender ES6. He usado Babel para transpilar a ES5 y ejecutar pruebas. Pero no estoy seguro de cómo proceder:

  1. ¿Transpilo y publico la carpeta resultante / fuera en NPM?
  2. ¿Incluyo la carpeta de resultados en mi repositorio de Github?
  3. ¿O mantengo 2 repositorios, uno con el código ES6 + script gulp para Github y otro con los resultados transpilados + pruebas para NPM?

En resumen: ¿qué pasos debo seguir para publicar un módulo escrito en ES6 en NPM, y al mismo tiempo permitir que la gente explore / bifurque el código original?

Travelling Tech Guy
fuente
He estado luchando con esta decisión últimamente. Veo la respuesta que marcó como correcta porque José también es el consenso.
Talves
Aquí está mi respuesta de 2018 , teniendo en cuenta el progreso con el soporte de módulos desde 2015.
Dan Dascalescu
1
Me encantaría poder hacer lo contrario. Use un módulo ES para importar un módulo NPM, pero estos son los únicos resultados que obtengo.
SeanMC

Respuestas:

81

El patrón que he visto hasta ahora es mantener los archivos es6 en un srcdirectorio y construir sus cosas en la publicación previa de npm en el libdirectorio.

Necesitará un archivo .npmignore, similar a .gitignore pero ignorando en srclugar de lib.

José F. Romaniello
fuente
44
¿Tienes un repositorio de ejemplo?
Ahmed Abbas
2
@JamesAkwuh Tenga en cuenta que es probable que desee cambiar los "Inicio" y comandos "construir" en el package.json utilizar la ruta relativa de la babel-cli: ./node_modules/babel-cli/bin/babel.js -s inline -d lib -w src. Esto debería garantizar que las instalaciones no fallen al implementar en nuevos entornos.
phazonNinja
2
@phazonNinja npm lo maneja
James Akwuh
44
"Si no hay un archivo .npmignore, pero hay un archivo .gitignore, entonces npm ignorará las cosas que coincidan con el archivo .gitignore". documentos oficiales de la npm
Frank Nocke
10
En lugar de .npmignoreusted, puede usar el filescampo en package.json . Le permite especificar exactamente los archivos que desea publicar, en lugar de buscar archivos aleatorios que no desea publicar.
Dan Dascalescu
76

Me gusta la respuesta de José. He notado que varios módulos siguen ese patrón ya. Así es como puede implementarlo fácilmente con Babel6. Lo instalo babel-clilocalmente para que la compilación no se rompa si alguna vez cambio mi versión global de babel.

.npmignore

/src/

.gitignore

/lib/
/node_modules/

Instalar Babel

npm install --save-dev babel-core babel-cli babel-preset-es2015

package.json

{
  "main": "lib/index.js",
  "scripts": {
    "prepublish": "babel src --out-dir lib"
  },
  "babel": {
    "presets": ["es2015"]
  }
}
Marius Craciunoiu
fuente
29
Cualquier comando en el scriptshabrá node_modules/.binagregado a su $PATHy desde que babel-cliinstala un binario node_modules/.bin/babelno hay necesidad de hacer referencia al comando por ruta.
Sukima
3
Tenga en cuenta que prepublishes problemático porque podría ejecutarse en el momento de la instalación ( github.com/npm/npm/issues/3059 ), prefiera el versiongancho de script más idiomático ( docs.npmjs.com/cli/version )
mattecapu
@mattecapu parece que el problema prepublishsigue ahí. Por el momento creo que compilar manualmente el srcdirectorio y npm publishes el camino a seguir.
sonlexqt
1
Puede usar el enlace deprepublishOnly secuencia de comandos (consulte docs.npmjs.com/misc/scripts#prepublish-and-prepare ). Tenga en cuenta que en la versión 5 de npm esto debería funcionar como se esperaba, pero por ahora (suponiendo que esté usando npm v4 +) esto debería funcionar.
Alex Mann
1
@FrankNocke se prepublishejecuta antes publish(obv.), Lo que lleva las cosas a npm (o donde sea que configure). Así que es para construir lo que incluye el paquete NPM, incluso si no está registrado.
Simon Buchan
42

TL; DR - No, hasta ~ octubre de 2019. El equipo de módulos de Node.js ha preguntado :

No publique ningún paquete de módulo ES destinado a ser utilizado por Node.js hasta [octubre de 2019]

Actualización de mayo de 2019

Desde 2015, cuando se hizo esta pregunta, el soporte de JavaScript para los módulos ha madurado significativamente y esperamos que sea oficialmente estable en octubre de 2019. Todas las demás respuestas ahora son obsoletas o demasiado complicadas. Aquí está la situación actual y las mejores prácticas.

Soporte ES6

El 99% de ES6 (también conocido como 2015) ha sido compatible con Node desde la versión 6 . La versión actual de Node es 12. Todos los navegadores de hoja perenne admiten la gran mayoría de las funciones de ES6. ECMAScript ahora está en la versión 2019 , y el esquema de versiones ahora favorece el uso de años.

Módulos ES (también conocidos como módulos ECMAScript) en navegadores

Todos los navegadores de hoja perenne han sido compatibles con los import módulos ES6 desde 2017. Las importaciones dinámicas son compatibles con Chrome (+ tenedores como Opera y Samsung Internet) y Safari. El soporte de Firefox está programado para la próxima versión, 67.

Ya no necesita Webpack / rollup / Parcel, etc. para cargar módulos. Pueden seguir siendo útiles para otros fines, pero no están obligados a cargar su código. Puede importar directamente las URL que apuntan al código de los módulos ES.

Módulos ES en Nodo

Los módulos ES ( .mjsarchivos con import/ export) han sido soportados desde el Nodo v8.5.0 llamando nodecon la --experimental-modulesbandera. Nodo v12, lanzado en abril de 2019, reescribió el soporte de módulos experimentales. El cambio más visible es que la extensión del archivo debe especificarse de forma predeterminada al importar:

// lib.mjs 

export const hello = 'Hello world!';

// index.mjs:

import { hello } from './lib.mjs';
console.log(hello);

Tenga en cuenta las .mjsextensiones obligatorias en todo. Correr como:

node --experimental-modules index.mjs

La versión Node 12 también es cuando el Equipo de Módulos solicitó a los desarrolladores que no publiquen paquetes de módulos ES destinados a ser utilizados por Node.js hasta que se encuentre una solución para usar paquetes a través de ambos require('pkg')y import 'pkg'. Todavía puede publicar módulos ES nativos destinados a navegadores.

Soporte de ecosistema de módulos ES nativos

A partir de mayo de 2019, el soporte del ecosistema para los módulos ES es inmaduro. Por ejemplo, los marcos de prueba como Jest y Ava no son compatibles --experimental-modules. Debe usar un transpilador, y luego debe decidir entre usar la import { symbol }sintaxis nombrada import ( ) (que todavía no funcionará con la mayoría de los paquetes npm), y la sintaxis predeterminada de importación ( import Package from 'package'), que funciona, pero no cuando Babel lo analiza para paquetes creados en TypeScript (graphql-tools, node-influx, faast, etc.) Sin embargo, existe una solución alternativa que funciona con --experimental-modulesy si Babel transpila su código para que pueda probarlo con Jest / Ava / Mocha, etc.

import * as ApolloServerM from 'apollo-server'; const ApolloServer = ApolloServerM.default || ApolloServerM;

Posiblemente feo, pero de esta manera puede escribir su propio código de módulos ES con import/ exporty ejecutarlo con node --experimental-modules, sin transpiladores. Si tiene dependencias que aún no están preparadas para ESM, impórtelas como se indica arriba y podrá usar marcos de prueba y otras herramientas a través de Babel.


Respuesta previa a la pregunta: recuerde, no haga esto hasta que Node resuelva el problema de requerir / importar, con suerte alrededor de octubre de 2019.

Publicación de módulos ES6 en npm, con compatibilidad con versiones anteriores

Para publicar un módulo ES en npmjs.org para que se pueda importar directamente, sin Babel u otros transpiladores, simplemente apunte el maincampo en package.jsonel .mjsarchivo, pero omita la extensión:

{
  "name": "mjs-example",
  "main": "index"
}

Ese es el único cambio. Al omitir la extensión, Node buscará primero un archivo mjs si se ejecuta con --experimental-modules. De lo contrario, volverá al archivo .js, por lo que su proceso de transpilación existente para admitir versiones de Nodo anteriores funcionará como antes, solo asegúrese de apuntar a Babel a los .mjsarchivos.

Aquí está la fuente de un módulo ES nativo con compatibilidad con versiones anteriores para Nodo <8.5.0 que publiqué en NPM. Puedes usarlo ahora mismo, sin Babel ni nada más.

Instale el módulo:

npm install local-iso-dt
# or, yarn add local-iso-dt

Cree un archivo de prueba test.mjs :

import { localISOdt } from 'local-iso-dt/index.mjs';
console.log(localISOdt(), 'Starting job...');

Ejecute el nodo (v8.5.0 +) con el indicador --experimental-modules:

node --experimental-modules test.mjs

Mecanografiado

Si desarrolla en TypeScript, puede generar código ES6 y usar módulos ES6:

tsc index.js --target es6 --modules es2015

Luego, debe cambiar *.jsel nombre de la salida a .mjs, un problema conocido que, con suerte, se solucionará pronto para que tscpueda enviar .mjsarchivos directamente.

Dan Dascalescu
fuente
3
Decir "Todos los navegadores de hoja perenne admiten la gran mayoría de las funciones de ES6". no significa mucho cuando nos fijamos en los datos y nos damos cuenta de que la compatibilidad con es6 en los navegadores solo llega al 80% de todos los usuarios.
Pedro Pedrosa
3
Actualmente, el ecosistema definitivamente no es lo suficientemente maduro para esto. El equipo de Node.js con el lanzamiento de v12 preguntó específicamente: "Por favor, no publique ningún paquete de módulo ES destinado a ser utilizado por Node.js hasta que esto se resuelva". 2ality.com/2019/04/nodejs-esm-impl.html#es-modules-on-npm Mocha no admite de forma nativa archivos .mjs. Muchas bibliotecas (por ejemplo, create-react-app, react-apollo, graphql-js) tuvieron problemas con las dependencias que contienen mjsarchivos. Node.js planea lanzar el soporte oficial en octubre de 2019, que es lo primero que volvería a revisar seriamente.
thisismydesign
3
@thisismydesign: gracias por el recordatorio de actualizar esta vieja respuesta! Acabo de hacerlo.
Dan Dascalescu
17

@Jose tiene razón. No hay nada de malo en publicar ES6 / ES2015 en NPM, pero eso puede causar problemas, especialmente si la persona que usa su paquete usa Webpack, por ejemplo, porque normalmente las personas ignoran la node_modulescarpeta durante el preprocesamiento babelpor razones de rendimiento.

Entonces, solo use gulp, grunto simplemente Node.js para construir una libcarpeta que sea ES5.

Aquí está mi build-lib.jsscript, que guardo ./tools/(no gulpo gruntaquí):

var rimraf = require('rimraf-promise');
var colors = require('colors');
var exec = require('child-process-promise').exec;

console.log('building lib'.green);

rimraf('./lib')
    .then(function (error) {
        let babelCli = 'babel --optional es7.objectRestSpread ./src --out-dir ./lib';
        return exec(babelCli).fail(function (error) {
            console.log(colors.red(error))
        });
    }).then(() => console.log('lib built'.green));

Aquí hay un último consejo: debe agregar un .npmignore a su proyecto . Si npm publishno encuentra este archivo, lo usará en su .gitignorelugar, lo que le causará problemas porque normalmente su .gitignorearchivo excluirá ./libe incluirá ./src, que es exactamente lo contrario de lo que desea cuando publica en NPM. El .npmignorearchivo tiene básicamente la misma sintaxis de .gitignore(AFAIK).

André Pena
fuente
1
En lugar de .npmignoreusted, puede usar el filescampo en package.json . Le permite especificar exactamente los archivos que desea publicar, en lugar de buscar archivos aleatorios que no desea publicar.
Dan Dascalescu
¿No se romperá este árbol?
Joe
No recomiendo usar .npmignore, intente package.jsonen su fileslugar, vea: github.com/c-hive/guides/blob/master/js/…
thisismydesign
@thisismydesign: ¿eso es exactamente lo que había recomendado en mi comentario anterior ...?
Dan Dascalescu
Mi mal, no me di cuenta :)
thisismydesign
8

Siguiendo el enfoque de José y Marius, (con la actualización de la última versión de Babel en 2019): mantenga los archivos JavaScript más recientes en un directorio src, y compile con el prepublishscript npm y la salida al directorio lib.

.npmignore

/src

.gitignore

/lib
/node_modules

Instalar Babel (versión 7.5.5 en mi caso)

$ npm install @babel/core @babel/cli @babel/preset-env --save-dev

package.json

{
  "name": "latest-js-to-npm",
  "version": "1.0.0",
  "description": "Keep the latest JavaScript files in a src directory and build with npm's prepublish script and output to the lib directory.",
  "main": "lib/index.js",
  "scripts": {
    "prepublish": "babel src -d lib"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.5.5",
    "@babel/core": "^7.5.5",
    "@babel/preset-env": "^7.5.5"
  },
  "babel": {
    "presets": [
      "@babel/preset-env"
    ]
  }
}

Y tengo src/index.jsque usa la función de flecha:

"use strict";

let NewOneWithParameters = (a, b) => {
  console.log(a + b); // 30
};
NewOneWithParameters(10, 20);

Aquí está el repositorio en GitHub .

Ahora puedes publicar el paquete:

$ npm publish
...
> latest-js-to-npm@1.0.0 prepublish .
> babel src -d lib

Successfully compiled 1 file with Babel.
...

Antes de publicar el paquete en npm, verá que lib/index.jsse ha generado, que se transpila a es5:

"use strict";

var NewOneWithParameters = function NewOneWithParameters(a, b) {
  console.log(a + b); // 30
};

NewOneWithParameters(10, 20);

[Actualización para el paquete acumulativo]

Como preguntó @kyw, ¿cómo integraría el paquete acumulativo?

Primero, instale rollupyrollup-plugin-babel

npm install -D rollup rollup-plugin-babel

En segundo lugar, cree rollup.config.jsen el directorio raíz del proyecto

import babel from "rollup-plugin-babel";

export default {
  input: "./src/index.js",
  output: {
    file: "./lib/index.js",
    format: "cjs",
    name: "bundle"
  },
  plugins: [
    babel({
      exclude: "node_modules/**"
    })
  ]
};

Por último, actualice prepublishenpackage.json

{
  ...
  "scripts": {
    "prepublish": "rollup -c"
  },
  ...
}

Ahora puede ejecutar npm publish, y antes de que el paquete se publique en npm, verá que se ha generado lib / index.js, que se transpila a es5:

'use strict';

var NewOneWithParameters = function NewOneWithParameters(a, b) {
  console.log(a + b); // 30
};

NewOneWithParameters(10, 20);

Nota: por cierto, ya no necesitas @babel/clisi estás usando el paquete acumulativo. Puede desinstalarlo de manera segura:

npm uninstall @babel/cli
Yuci
fuente
¿Cómo integraría Rollup bundler?
kyw
1
@kyw, sobre cómo integrar Rollup bundler, vea mi respuesta actualizada.
Yuci
Actualización de diciembre de 2019 -> github.com/rollup/rollup/blob/…
a.barbieri
6

Si desea ver esto en acción en un módulo Node de código abierto pequeño muy simple, eche un vistazo al enésimo día (que comencé, también a otros contribuyentes). Busque en el archivo package.json y en el paso de prepublicación que lo llevará a dónde y cómo hacerlo. Si clonas ese módulo, puedes ejecutarlo localmente y usarlo como plantilla para ti.

Chico
fuente
4

Node.js 13.2.0+ es compatible con ESM sin el indicador experimental y hay algunas opciones para publicar paquetes NPM híbridos (ESM y CommonJS) (según el nivel de compatibilidad con versiones anteriores necesario): https://2ality.com/2019 /10/hybrid-npm-packages.html

Recomiendo ir a la compatibilidad con versiones anteriores para facilitar el uso de su paquete. Esto podría verse de la siguiente manera:

El paquete híbrido tiene los siguientes archivos:

mypkg/
  package.json
  esm/
    entry.js
  commonjs/
    package.json
    entry.js

mypkg/package.json

{
  "type": "module",
  "main": "./commonjs/entry.js",
  "exports": {
    "./esm": "./esm/entry.js"
  },
  "module": "./esm/entry.js",
  ···
}

mypkg/commonjs/package.json

{
  "type": "commonjs"
}

Importando desde CommonJS:

const {x} = require('mypkg');

Importando desde ESM:

import {x} from 'mypkg/esm';

Hicimos una investigación sobre el soporte de ESM en 05.2019 y descubrimos que muchas bibliotecas carecían de soporte (de ahí la recomendación de compatibilidad con versiones anteriores):

este es mi diseño
fuente
Parece que no puedo importar módulos ES6 instalados globalmente en la carpeta node_modules (proporcionada por npm root -g). ¿Realmente no se supone que podamos hacer eso? Estoy realmente confundido Sé que npm link puede resolver el problema al vincular el módulo a mi carpeta local node_modules, pero quiero saber por qué no se admite la importación de módulos de nodo global.
Joakim L. Christiansen
Respondiéndome a mí mismo, supongo que nunca será compatible: github.com/nodejs/node-eps/blob/master/… Sin embargo , es una decisión realmente estúpida, sería fácil de apoyar ...
Joakim L. Christiansen
3

Los dos criterios de un paquete NPM es que se puede usar con nada más que require( 'package' )ay hace algo de software.

Si cumple con estos dos requisitos, puede hacer lo que desee. Incluso si el módulo está escrito en ES6, si el usuario final no necesita saber eso, lo transpilaría por ahora para obtener el máximo soporte.

Sin embargo, si como koa , su módulo requiere compatibilidad con usuarios que usan las funciones de ES6, entonces quizás la solución de dos paquetes sería una mejor idea.

Para llevar

  1. Solo publique la cantidad de código que necesite para que require( 'your-package' )funcione.
  2. A menos que el ES5 y 6 sean importantes para el usuario, solo publique 1 paquete. Transpílalo si es necesario.
JoshWillik
fuente
1
Esto no parece responder a la pregunta. Creo que el OP está tratando de descubrir cómo estructurar su repositorio de Github y qué publicar en NPM y todo lo que has dicho es que pueden hacer lo que quieran. El OP quiere recomendaciones específicas sobre una buena práctica para esta situación.
jfriend00
@ jfriend00 No estoy de acuerdo. Le recomendé que transpile y solo publique los archivos necesarios para require( 'package' )trabajar. Editaré mi respuesta para aclarar esto. Dicho esto, la respuesta de José es mucho mejor que la mía.
JoshWillik
La respuesta de José es muy buena, pero agradezco esta por describir explícitamente buenas reglas generales para cuándo / por qué usar uno o dos paquetes.
Jordan Gray
3

La clave principal en package.jsondecide el punto de entrada al paquete una vez publicado. Por lo tanto, puede colocar la salida de su Babel donde quiera y solo tiene que mencionar la ruta correcta en mainclave.

"main": "./lib/index.js",

Aquí hay un artículo bien escrito sobre cómo publicar un paquete npm

https://codeburst.io/publish-your-own-npm-package-ff918698d450

Aquí hay un repositorio de muestra que puede usar como referencia

https://github.com/flexdinesh/npm-module-boilerplate

Dinesh Pandiyan
fuente
Esa publicación omite el hecho de que puede (y debe) publicar en módulos NPM creados en ES6 y que pueden hacerlo importdirectamente sin requerir Babel ni ningún otro transpilador.
Dan Dascalescu
0

Dependiendo de la anatomía de su módulo, esta solución puede no funcionar, pero si su módulo está contenido dentro de un solo archivo y no tiene dependencias (no hace uso de la importación ), con el siguiente patrón puede liberar su código tal como está , y podrá importarse con import (módulos ES6 del navegador) y requerir (módulos Node CommonJS)

Como beneficio adicional, será adecuado importarlo utilizando un elemento HTML SCRIPT.

main.js :

(function(){
    'use strict';
    const myModule = {
        helloWorld : function(){ console.log('Hello World!' )} 
    };

    // if running in NODE export module using NODEJS syntax
    if(typeof module !== 'undefined') module.exports = myModule ;
    // if running in Browser, set as a global variable.
    else window.myModule = myModule ;
})()

my-module.js :

    // import main.js (it will declare your Object in the global scope)
    import './main.js';
    // get a copy of your module object reference
    let _myModule = window.myModule;
    // delete the the reference from the global object
    delete window.myModule;
    // export it!
    export {_myModule as myModule};

package.json : `

    {
        "name" : "my-module", // set module name
        "main": "main.js",  // set entry point
        /* ...other package.json stuff here */
    }

Para usar su módulo, ahora puede usar la sintaxis regular ...

Cuando se importa en NODE ...

    let myModule = require('my-module');
    myModule.helloWorld();
    // outputs 'Hello World!'

Cuando se importa en NAVEGADOR ...

    import {myModule} from './my-module.js';
    myModule.helloWorld();
    // outputs 'Hello World!'

O incluso cuando se incluye utilizando un elemento de secuencia de comandos HTML ...

<script src="./main.js"></script>
<script>
     myModule.helloWorld();
    // outputs 'Hello World!'
</script>
colxi
fuente
-1

Algunas notas adicionales para cualquier persona, utilizando módulos propios directamente desde github, sin pasar por los módulos publicados :

El gancho ( ampliamente utilizado ) "prepublish" no está haciendo nada por usted.

Lo mejor que se puede hacer (si planea confiar en repositorios de github, no en cosas publicadas):

  • anular srcde .npmignore (en otras palabras: permitirlo). Si no tiene un .npmignore, recuerde: .gitignorese utilizará una copia de en su lugar en la ubicación instalada, como ls node_modules/yourProjectse mostrará.
  • asegúrese de que babel-clisea ​​una dependencia en su módulo, no solo un DevDepenceny, ya que realmente está construyendo en la máquina consumidora, también conocida como la computadora de los desarrolladores de aplicaciones, que está utilizando su módulo
  • hacer lo de compilación, en el gancho de instalación, es decir:

    "install": "babel src -d lib -s"

(sin valor agregado al intentar algo "preinstalar", es decir, puede faltar babel-cli)

Frank Nocke
fuente
3
Compilar en la instalación es muy grosero. Por favor, no hagas esto: ¡el tiempo de instalación de npm es bastante malo! Para el código interno donde desea evitar el uso de un repositorio de paquetes npm, puede: 1) usar un monorepo, 2) Cargar y depender de la npm packsalida, 3) verificar la salida de compilación.
Simon Buchan