Sobre el consumo (y publicación) de paquetes ES2015+
Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
Para quienes necesitamos dar soporte a navegadores antiguos, ejecutamos un compilador como Babel sobre nuestro código de aplicación. Pero ese no es todo el código que enviamos a los navegadores; también está el código en nuestros node_modules.
¿Podemos hacer que compilar nuestras dependencias no solo sea posible, sino algo normal?
La capacidad de compilar dependencias es una característica habilitadora para todo el ecosistema. Partiendo de los cambios que implementamos en Babel v7 para permitir la compilación selectiva de dependencias, esperamos ver esto estandarizado en el futuro.
Supuestos
-
Enviamos a navegadores modernos que soportan ES2015+ nativamente (no necesitan soportar IE) o pueden enviar múltiples tipos de bundles (por ejemplo, usando
<script type="module">y<script nomodule>). -
Nuestras dependencias realmente publican ES2015+ en lugar del estándar actual de ES5/ES3.
-
El estándar futuro no debería fijarse en ES2015, sino ser un objetivo cambiante.
Por qué
¿Por qué es deseable compilar dependencias (en lugar de solo nuestro propio código)?
-
Para tener libertad al decidir los trade-offs sobre dónde puede ejecutarse el código (vs. la biblioteca).
-
Para enviar menos código a los usuarios, dado que JavaScript tiene un costo.
El entorno de ejecución efímero de JavaScript
El argumento sobre por qué compilar dependencias sería útil es el mismo por el que Babel eventualmente introdujo @babel/preset-env. Vimos que los desarrolladores querrían avanzar más allá de solo compilar a ES5.
Babel solía llamarse 6to5, ya que solo convertía de ES2015 (conocido como ES6 en ese entonces) a ES5. En aquel momento, el soporte en navegadores para ES2015 era casi inexistente, por lo que la idea de un compilador de JavaScript era novedosa y útil: podíamos escribir código moderno y hacerlo funcionar para todos nuestros usuarios.
¿Pero qué pasa con los propios entornos de ejecución del navegador? Dado que los navegadores evergreen eventualmente alcanzan el estándar (como lo hicieron con ES2015), crear preset-env ayuda a Babel y a la comunidad a alinearse tanto con los navegadores como con el propio TC39. Si solo compiláramos a ES5, nadie ejecutaría código nativo en los navegadores.
La verdadera diferencia es darse cuenta de que siempre habrá una ventana móvil de soporte:
-
Código de aplicación (nuestros entornos soportados)
-
Navegadores (Chrome, Firefox, Edge, Safari)
-
Babel (la capa de abstracción)
-
Propuestas de TC39/ECMAScript (e implementaciones de Babel)
Por lo tanto, la necesidad no es solo que 6to5 se renombre a Babel porque compila a 7to5, sino que Babel cambie el supuesto implícito de que solo apunta a ES5. ¡Con @babel/preset-env, podemos escribir el último JavaScript y apuntar a cualquier navegador/entorno!
Usar Babel y preset-env nos ayuda a mantenernos al día con esa ventana móvil en constante cambio. Sin embargo, incluso si lo usamos, actualmente solo se aplica a nuestro código de aplicación, no a las dependencias de nuestro código.
¿De quién son nuestras dependencias?
Como tenemos control sobre nuestro propio código, podemos aprovechar preset-env: tanto escribiendo en ES2015+ como dirigiendo a navegadores que soportan ES2015+.
Este no es necesariamente el caso para nuestras dependencias; para obtener los mismos beneficios que al compilar nuestro código, es posible que necesitemos hacer algunos cambios.
¿Es tan sencillo como simplemente ejecutar Babel sobre node_modules?
Complejidades actuales al compilar dependencias
Complejidad del compilador
Aunque esto no debería disuadirnos de hacerlo posible, debemos ser conscientes de que compilar dependencias aumenta la superficie de problemas y complejidad, especialmente para el propio Babel.
-
Los compiladores no son diferentes a otros programas y tienen errores.
-
No todas las dependencias necesitan ser compiladas, y compilar más archivos sí significa una construcción más lenta.
-
El propio
preset-envpodría tener errores porque usamoscompat-tablepara nuestros datos frente a Test262 (el conjunto de pruebas oficial). -
Los navegadores mismos pueden tener problemas al ejecutar código ES2015+ nativo versus ES5.
-
Todavía existe la cuestión de determinar qué se considera "soportado": ver babel/babel-preset-env#54 para un ejemplo de caso límite. ¿Pasa la prueba solo porque analiza sintácticamente o tiene soporte parcial?
Problemas específicos en Babel v6
Ejecutar un script como un module puede causar un SyntaxError, nuevos errores en tiempo de ejecución o comportamiento inesperado debido a las diferencias semánticas entre scripts clásicos y módulos.
Babel v6 consideraba cada archivo como un module y por tanto en "modo estricto".
Se podría argumentar que esto es realmente algo bueno, ya que todos los que usan Babel están optando por modo estricto por defecto 🙂.
Ejecutar Babel con una configuración convencional sobre todos nuestros node_modules puede causar problemas con código que es un script, como un plugin de jQuery.
Un ejemplo de problema es cómo this se convierte en undefined.
// Input
(function($) {
// …
}(this.jQuery));
// Output
"use strict";
(function ($) {
// …
})(undefined.jQuery);
Esto fue cambiado en v7 para que no inyecte automáticamente la directiva "use strict" a menos que el archivo fuente sea un module.
Tampoco estaba en el alcance original de Babel compilar dependencias: de hecho recibimos informes de que personas lo hacían accidentalmente, ralentizando la construcción. Hay muchos valores predeterminados y documentación en las herramientas que deshabilitan intencionalmente la compilación de node_modules.
Usando sintaxis no estándar
Existen muchos problemas al distribuir sintaxis de propuestas no compiladas (esta publicación fue inspirada por la preocupación de Dan al respecto).
Proceso de etapas
El proceso de etapas de TC39 no siempre avanza: una propuesta puede moverse a cualquier punto del proceso: incluso retroceder de Etapa 3 a Etapa 2 como ocurrió con Separadores Numéricos (1_000), abandonarse por completo (Object.observe(), y otros que quizás hemos olvidado 😁), o simplemente estancarse como el enlace de funciones (a::b) o los decoradores hasta hace poco.
- Resumen de las Etapas: La Etapa 0 no tiene criterios y significa que la propuesta es solo una idea, la Etapa 1 implica aceptar que el problema merece solución, la Etapa 2 consiste en describir una solución en texto de especificación, la Etapa 3 significa que la solución específica está bien definida, y la Etapa 4 indica que está lista para inclusión en la especificación con pruebas, múltiples implementaciones en navegadores y experiencia en campo.
Uso de Propuestas
— Rach Smith 🌈 (@rachsmithtweets) August 1, 2017
Ya recomendamos que los desarrolladores sean cautelosos al usar propuestas inferiores a la Etapa 3, y mucho menos publicarlas.
Pero solo decirle a la gente que no use la Etapa X contradice el propósito mismo de Babel. Una razón clave por la que las propuestas mejoran y avanzan es la retroalimentación que el comité recibe del uso real (ya sea en producción o no) basado en su implementación a través de Babel.
Ciertamente hay un equilibrio necesario: no queremos disuadir a la gente de usar nueva sintaxis (eso es difícil de vender 😂), pero tampoco queremos que asuman que "una vez en Babel, la sintaxis es oficial o inmutable". Idealmente, los usuarios investigarán el propósito de una propuesta y evaluarán las compensaciones para su caso de uso.
Eliminación de los Presets de Etapa en v7
Aunque una práctica común es usar el preset de Etapa 0, planeamos eliminar los presets de etapa en v7. Inicialmente pensamos que sería conveniente, que la gente crearía sus propios presets no oficiales, o que ayudaría con la "fatiga de JavaScript". Pero parece causar más problemas: los usuarios siguen copiando/pegando configuraciones sin entender qué contiene un preset.
Al fin y al cabo, ver "stage-0" no significa nada. Mi esperanza es que al hacer explícita la decisión de usar plugins de propuestas, los usuarios deberán aprender qué sintaxis no estándar están adoptando. Intencionalmente, esto debería fomentar un mejor entendimiento no solo de Babel sino de JavaScript como lenguaje y su evolución, más allá de solo su uso.
Publicación de Sintaxis No Estándar
Como autores de bibliotecas, publicar sintaxis no estándar expone a nuestros usuarios a posibles inconsistencias, refactorizaciones y rupturas en sus proyectos. Como una propuesta de TC39 (incluso en Etapa 3) puede cambiar, inevitablemente tendremos que modificar el código de la biblioteca. Una propuesta "nueva" no significa que la idea sea fija o definitiva, sino que queremos explorar colectivamente el espacio de soluciones.
Al menos si distribuimos la versión compilada, seguirá funcionando, y el mantenedor puede modificar la salida para que compile a código funcionalmente equivalente. Distribuir la versión sin compilar implica que cualquier consumidor necesita un paso de compilación y la misma configuración de Babel que nosotros. Esto es análogo a usar TS/JSX/Flow: no esperaríamos que los consumidores configuraran el mismo entorno de compilación solo porque nosotros los usamos.
Confusión entre Módulos JavaScript y ES2015+
Cuando escribimos import foo from "foo" o require("foo") y foo no tiene index.js, se resuelve al campo main en el package.json del módulo.
Herramientas como Rollup/webpack también leen otro campo llamado module (antes jsnext:main). Lo usan para resolver al archivo de Módulo JS.
- Ejemplo con
redux
// redux package.json
{
...
"main": "lib/redux.js", // ES5 + Common JS
"module": "es/redux.js", // ES5 + JS Modules
}
Esto se introdujo para que los usuarios pudieran consumir Módulos JS (ESM).
Sin embargo, la única intención de este campo es ESM, no otra cosa. La documentación de Rollup especifica que el campo module aclara que no está destinado a otras características futuras de JavaScript.
Pese a esta advertencia, los autores de paquetes invariablemente confunden el uso de módulos ES con el nivel de lenguaje JavaScript utilizado al escribirlos.
Por lo tanto, podríamos necesitar otra forma de indicar el nivel de lenguaje.
¿Soluciones no escalables?
Una sugerencia común es que las bibliotecas publiquen ES2015 bajo otro campo como es2015, por ejemplo: "es2015": "es2015/package.mjs".
// @angular/core package.json
{
"main": "./bundles/core.umd.js",
"module": "./fesm5/core.js",
"es2015": "./fesm2015/core.js",
"esm5": "./esm5/core.js",
"esm2015": "./esm2015/core.js",
"fesm5": "./fesm5/core.js",
"fesm2015": "./fesm2015/core.js",
}
Esto funciona para ES2015, pero plantea la pregunta: ¿qué hacemos con ES2016? ¿Debemos crear una nueva carpeta por cada año y un nuevo campo en package.json? Parece insostenible y seguirá produciendo node_modules más grandes.
Esto fue un problema con Babel mismo: planeábamos seguir publicando presets anuales (
preset-es2015,preset-es2016...) hasta que nos dimos cuenta de quepreset-enveliminaría esa necesidad.
Publicar basado en entornos/sintaxis específicos sería igualmente insostenible ya que la cantidad de combinaciones solo aumenta ("ie-11-arrow-functions").
¿Qué tal distribuir solo el código fuente? Podría tener problemas similares si usamos sintaxis no estándar como mencionamos antes.
Tener un campo esnext tampoco sería completamente útil. La versión "más reciente" de JavaScript cambia según el momento en que escribimos el código.
Las dependencias podrían no publicar ES2015+
Este esfuerzo solo será estándar si resulta sencillo de aplicar para autores de bibliotecas. Será difícil argumentar la importancia de este cambio si bibliotecas nuevas y populares no pueden usar la sintaxis más reciente.
Debido a la complejidad y configuración de herramientas, puede ser difícil que los proyectos publiquen ES2015+/ESM. Este es probablemente el mayor desafío, y añadir más documentación no es suficiente.
Para Babel, podríamos necesitar añadir funcionalidades a @babel/cli para facilitar esto, o quizás hacer que el paquete babel lo haga por defecto. También podríamos integrarnos mejor con herramientas como microbundle de @developit.
¿Y cómo manejamos los polyfills (esto será un próximo post)? ¿Cómo sería para un autor de biblioteca (o usuario) no tener que pensar en polyfills?
Dicho todo esto, ¿cómo ayuda Babel con esto?
Cómo ayuda Babel v7
Como discutimos, compilar dependencias en Babel v6 puede ser complicado. Babel v7 abordará algunos de estos problemas.
Un problema es la búsqueda de configuración. Babel actualmente funciona por archivo, por lo que al compilar busca la configuración más cercana (.babelrc) escalando el árbol de directorios si no la encuentra en la carpeta actual.
project
└── .babelrc // closest config for a.js
└── a.js
└── node_modules
└── package
└── .babelrc // closest config for b.js
└── b.js
Hicimos algunos cambios:
-
Detener la búsqueda en el límite del paquete (al encontrar un
package.json). Esto evita que Babel cargue configuraciones fuera de la aplicación, especialmente sorprendente cuando encuentra una en el directorio de usuario. -
En monorepos, podríamos querer un
.babelrcpor paquete que herede de una configuración central. -
Babel mismo es un monorepo, así que usamos el nuevo
babel.config.jsque resuelve todos los archivos con esa configuración (sin búsqueda).
Compilación selectiva con "overrides"
Añadimos una opción "overrides" que permite crear configuraciones específicas para conjuntos de rutas de archivos.
Esto permite que cada objeto de configuración especifique un campo
test/include/exclude, igual que harías en Webpack. Cada elemento permite un ítem o un array de ítems que pueden serstring,RegExpofunction.
Esto nos permite tener una única configuración para toda nuestra aplicación: quizás queremos compilar nuestro código JavaScript del servidor de manera diferente al código del cliente (además de compilar algunos paquetes en node_modules).
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
targets: { node: 'current' },
}],
],
overrides: [{
test: ["./client-code", "./node_modules/package-a"],
presets: [
['@babel/preset-env', {
targets: { "chrome": "60" } },
}],
],
}],
}
Recomendaciones para debatir
Deberíamos cambiar nuestra visión fija de publicar JavaScript por una que se mantenga al día con el estándar más reciente.
Deberíamos seguir publicando ES5/CJS bajo main para compatibilidad con herramientas actuales, pero también publicar una versión compilada con la sintaxis más reciente (sin propuestas experimentales) bajo una nueva clave que podamos estandarizar como main-es. (No creo que module deba ser esa clave ya que estaba pensada solo para Módulos JS).
Quizás deberíamos decidir otra clave en
package.json, ¿tal vez"es"? Me recuerda a la encuesta que hice sobre babel-preset-latest.
Compilar dependencias no es algo que solo un proyecto/empresa pueda aprovechar: requiere un impulso de toda la comunidad para avanzar. Aunque este esfuerzo será natural, podría requerir cierta estandarización: podemos implementar criterios para que las bibliotecas opten por publicar ES2015+ y verificarlo mediante CI/herramientas/npm mismo.
La documentación necesita actualizarse para mencionar los beneficios de compilar node_modules, cómo hacerlo para autores de bibliotecas y cómo consumirlo en bundlers/compiladores.
Y con Babel 7, los consumidores pueden usar preset-env de forma más segura y optar por aplicarlo a node_modules con nuevas opciones de configuración como overrides.
¡Hagámoslo!
¡Compilar JavaScript no debería ser solo sobre la distinción específica ES2015/ES5, ya sea para nuestra aplicación o nuestras dependencias! Esperamos que esta sea una llamada alentadora a la acción para reiniciar conversaciones sobre el uso de dependencias publicadas en ES2015+ como ciudadanos de primera clase.
Esta publicación explora algunas formas en que Babel debería ayudar en este esfuerzo, pero necesitaremos la ayuda de todos para cambiar el ecosistema: más educación, más paquetes publicados mediante opt-in y mejores herramientas.
Agradecimientos a las muchas personas que ofrecieron revisar esta publicación, incluyendo a @chrisdarroch, @existentialism, @mathias, @betaorbust, @_developit, @jdalton, @bonsaistudio.