Vai al contenuto principale

Sull'Utilizzo (e la Pubblicazione) di Pacchetti ES2015+

· Lettura di 14 min
Traduzione Beta Non Ufficiale

Questa pagina è stata tradotta da PageTurner AI (beta). Non ufficialmente approvata dal progetto. Hai trovato un errore? Segnala problema →

Per chi deve supportare browser obsoleti, eseguiamo un compilatore come Babel sul codice applicativo. Ma non è tutto il codice che distribuiamo ai browser: c'è anche il codice nei nostri node_modules.

Possiamo rendere la compilazione delle dipendenze non solo possibile, ma normale?

La capacità di compilare le dipendenze è una funzionalità abilitante per l'intero ecosistema. Partendo dai cambiamenti introdotti in Babel v7 per consentire la compilazione selettiva delle dipendenze, speriamo di vederla standardizzata in futuro.

Ipotesi

  • Distribuiamo a browser moderni che supportano ES2015+ nativamente (non devono supportare IE) o possono ricevere bundle multipli (ad esempio usando <script type="module"> e <script nomodule>).

  • Le nostre dipendenze pubblicano effettivamente ES2015+ invece dell'attuale baseline ES5/ES3.

  • La baseline futura non dovrebbe essere fissata a ES2015, ma rappresenta un obiettivo in evoluzione.

Perché

Perché è auspicabile compilare le dipendenze (anziché solo il nostro codice)?

  • Per avere libertà nel bilanciare dove il codice può essere eseguito (rispetto alla libreria).

  • Per distribuire meno codice agli utenti, poiché JavaScript ha un costo.

L'Effimero Runtime JavaScript

Il motivo per cui compilare le dipendenze sarebbe utile è lo stesso per cui Babel ha alla fine introdotto @babel/preset-env. Abbiamo osservato che gli sviluppatori vorrebbero superare la sola compilazione verso ES5.

Babel si chiamava originariamente 6to5, poiché convertiva solo da ES2015 (allora ES6) a ES5. All'epoca, il supporto browser per ES2015 era quasi inesistente, quindi un compilatore JavaScript risultava innovativo e utile: potevamo scrivere codice moderno funzionante per tutti gli utenti.

Ma i runtime browser stessi? Poiché i browser evergreen raggiungeranno lo standard (come avvenuto per ES2015), preset-env aiuta Babel e la comunità ad allinearsi sia con i browser che con TC39. Compilando solo verso ES5, nessuno eseguirebbe mai codice nativo nei browser.

La differenza cruciale è comprendere che esisterà sempre una finestra mobile di supporto:

  • Codice applicativo (i nostri ambienti supportati)

  • Browser (Chrome, Firefox, Edge, Safari)

  • Babel (lo strato di astrazione)

  • Proposte TC39/ECMAScript (e implementazioni Babel)

Quindi non serve solo rinominare 6to5 in Babel perché compila in 7to5, ma modificare l'assunzione implicita che Babel targettizzi solo ES5. Con @babel/preset-env, possiamo scrivere JavaScript moderno e targettizzare qualsiasi browser/ambiente!

Babel e preset-env ci aiutano a seguire questa finestra mobile in evoluzione. Tuttavia, anche utilizzandoli, attualmente servono solo per il nostro codice applicativo, non per le dipendenze.

Di chi sono le nostre dipendenze?

Poiché abbiamo il controllo del nostro codice, possiamo sfruttare preset-env: sia scrivendo in ES2015+ sia mirando a browser che supportano ES2015+.

Questo non è necessariamente vero per le nostre dipendenze; per ottenere gli stessi vantaggi della compilazione del nostro codice, potremmo dover apportare alcune modifiche.

È semplice come eseguire Babel su node_modules?

Complessità attuali nella compilazione delle dipendenze

Complessità del compilatore

Anche se non dovrebbe impedirci di renderlo possibile, dobbiamo essere consapevoli che compilare le dipendenze aumenta la superficie di problemi e complessità, specialmente per Babel stesso.

  • I compilatori non sono diversi da altri programmi e hanno bug.

  • Non tutte le dipendenze hanno bisogno di essere compilate, e compilare più file significa un build più lento.

  • preset-env stesso potrebbe avere bug perché usiamo compat-table per i nostri dati rispetto a Test262 (la suite di test ufficiale).

  • I browser stessi possono avere problemi nell'eseguire codice ES2015+ nativo rispetto a ES5.

  • Rimane la questione di determinare cosa è "supportato": vedi babel/babel-preset-env#54 per un esempio di caso limite. Supera il test solo perché analizza correttamente o ha un supporto parziale?

Problemi specifici in Babel v6

Eseguire uno script come module può causare un SyntaxError, nuovi errori a runtime o comportamenti inattesi a causa delle differenze semantiche tra script classici e moduli.

Babel v6 considerava ogni file come un module e quindi in "strict mode".

Si potrebbe sostenere che sia una cosa positiva, dato che chi usa Babel accetta lo strict mode per default 🙂.

Eseguire Babel con una configurazione convenzionale su tutti i nostri node_modules potrebbe causare problemi con codice che è uno script, come un plugin jQuery.

Un esempio di problema è come this venga convertito in undefined.

JavaScript
// Input
(function($) {
// …
}(this.jQuery));
JavaScript
// Output
"use strict";

(function ($) {
// …
})(undefined.jQuery);

Questo è stato modificato in v7 in modo che non inietti automaticamente la direttiva "use strict" a meno che il file sorgente non sia un module.

Inoltre, compilare le dipendenze non era nello scopo originale di Babel: abbiamo ricevuto segnalazioni di problemi perché gli utenti lo facevano accidentalmente, rallentando il build. Ci sono molte impostazioni predefinite e documentazioni negli strumenti che disabilitano deliberatamente la compilazione di node_modules.

Utilizzo di sintassi non standard

Ci sono molti problemi nel distribuire sintassi di proposte non compilate (questo post è stato ispirato dalla preoccupazione di Dan su questo tema).

Processo di staging

Il processo di staging di TC39 non procede sempre in avanti: una proposta può muoversi in qualsiasi punto del processo: persino tornare indietro dallo Stage 3 allo Stage 2 come nel caso dei Numeric Separators (1_000), essere abbandonata del tutto (Object.observe(), e altre che potremmo aver dimenticato 😁), o semplicemente bloccarsi come function bind (a::b) o i decoratori fino a poco tempo fa.

Riepilogo degli stage: lo Stage 0 non ha criteri e indica che la proposta è solo un'idea, lo Stage 1 implica che il problema merita una soluzione, lo Stage 2 descrive una soluzione nel testo delle specifiche, lo Stage 3 significa che la soluzione specifica è stata elaborata e lo Stage 4 indica che è pronta per l'inclusione nelle specifiche con test, implementazioni su più browser ed esperienza sul campo.

Utilizzo delle proposte

Raccomandiamo già di prestare attenzione quando si utilizzano proposte inferiori allo Stage 3, figuriamoci pubblicarle.

Ma dire semplicemente alle persone di non usare lo Stage X contraddice lo scopo stesso di Babel. Un motivo cruciale per cui le proposte migliorano e progrediscono è il feedback che il comitato riceve dall'uso nel mondo reale (in produzione o meno) basato sull'utilizzo tramite Babel.

C'è sicuramente un equilibrio da trovare: non vogliamo dissuadere le persone dall'usare nuove sintassi (una cosa difficile da vendere 😂), ma nemmeno che credano che "una volta in Babel, la sintassi sia ufficiale o immutabile". Idealmente, le persone dovrebbero valutare lo scopo di una proposta e fare compromessi in base al loro caso d'uso.

Rimozione dei preset per gli stage in v7

Nonostante l'utilizzo del preset Stage 0 sia tra le pratiche più comuni, intendiamo rimuovere i preset per gli stage in v7. Inizialmente pensavamo fossero convenienti, che le persone avrebbero comunque creato preset non ufficiali, o che avrebbero mitigato la "JavaScript fatigue". Sembrano invece causare più problemi: le persone continuano a copiare/incollare configurazioni senza capire cosa contenga un preset.

Dopotutto, vedere "stage-0" non comunica nulla. Spero che rendendo esplicita la decisione di usare plugin per proposte, le persone dovranno apprendere quale sintassi non standard stanno adottando. Intenzionalmente, questo dovrebbe portare a una migliore comprensione non solo di Babel ma di JavaScript come linguaggio e del suo sviluppo, oltre che del suo utilizzo.

Pubblicazione di sintassi non standard

Come autori di librerie, pubblicare sintassi non standard espone gli utenti a possibili inconsistenze, refactoring e rotture dei loro progetti. Poiché una proposta TC39 (anche allo Stage 3) potrebbe cambiare, dovremo inevitabilmente modificare il codice della libreria. Una proposta "nuova" non indica un'idea definitiva, ma che vogliamo esplorare collettivamente lo spazio delle soluzioni.

Almeno distribuendo la versione compilata, funzionerà ancora, e il maintainer potrà modificare l'output per compilarlo in un codice funzionalmente identico. Distribuire la versione non compilata obbliga chi consuma un pacchetto a introdurre un build step e a replicare la nostra configurazione di Babel. È analogo all'uso di TS/JSX/Flow: non ci aspetteremmo che i consumer configurino lo stesso ambiente di compilazione solo perché li abbiamo usati noi.

Confusione tra moduli JavaScript e ES2015+

Quando scriviamo import foo from "foo" o require("foo") e foo non ha un index.js, la risoluzione avviene tramite il campo main nel package.json del modulo.

Strumenti come Rollup/webpack leggono anche un altro campo chiamato module (precedentemente jsnext:main), utilizzandolo per risolvere il file del modulo ES.

JavaScript
// redux package.json
{
...
"main": "lib/redux.js", // ES5 + Common JS
"module": "es/redux.js", // ES5 + JS Modules
}

Questo campo è stato introdotto per consentire il consumo di moduli ES (ESM).

Tuttavia, l'unico scopo di questo campo è l'ESM, nient'altro. La documentazione di Rollup specifica che il campo module chiarisce che non è destinato a future sintassi JavaScript.

Nonostante l'avvertimento, gli autori di pacchetti confondono sistematicamente l'uso dei moduli ES con il livello del linguaggio JavaScript utilizzato per scriverli.

Pertanto, potremmo aver bisogno di un altro modo per indicare il livello del linguaggio.

Soluzioni non scalabili?

Una soluzione comune consiste nel far pubblicare alle librerie ES2015 in un altro campo come es2015, ad esempio "es2015": "es2015/package.mjs".

JavaScript
// @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",
}

Questo funziona per ES2015, ma solleva la questione di cosa dovremmo fare per ES2016? Dovremmo creare una nuova cartella per ogni anno e un nuovo campo in package.json? Sembra insostenibile e continuerà a produrre node_modules più grandi.

Questo è stato un problema per Babel stesso: avevamo intenzione di continuare a pubblicare preset annuali (preset-es2015, preset-es2016..) finché non ci siamo resi conto che preset-env avrebbe eliminato questa necessità.

Pubblicarlo in base ad ambienti/sintassi specifici sembrerebbe altrettanto insostenibile poiché il numero di combinazioni aumenta solo ("ie-11-arrow-functions").

E se distribuissimo solo il sorgente stesso? Potrebbe avere problemi simili se usassimo sintassi non standard come menzionato in precedenza.

Anche avere un campo esnext potrebbe non essere del tutto utile. La versione "più recente" di JavaScript cambia a seconda del momento in cui abbiamo scritto il codice.

Le dipendenze potrebbero non pubblicare ES2015+

Questo sforzo diventerà uno standard solo se diventerà semplice da applicare per un autore di librerie. Sarà difficile sostenere l'importanza di questo cambiamento se sia le librerie nuove che quelle popolari non sono in grado di distribuire la sintassi più recente.

A causa della complessità e della configurazione degli strumenti, potrebbe essere difficile per i progetti pubblicare ES2015+/ESM. Questo è probabilmente il problema più grande da risolvere, e aggiungere più documentazione non è sufficiente.

Per Babel, potremmo dover aggiungere alcune richieste di funzionalità a @babel/cli per semplificare questo processo, e magari fare in modo che il pacchetto babel lo faccia di default? Oppure dovremmo integrare meglio strumenti come microbundle di @developit.

E come gestiamo i polyfill (questo sarà un post futuro)? Come sarebbe per un autore di librerie (o per l'utente) non dover pensare ai polyfill?

Detto tutto questo, come può Babel aiutare in tutto ciò?

Come Babel v7 aiuta

Come abbiamo discusso, compilare le dipendenze in Babel v6 può essere piuttosto doloroso. Babel v7 affronterà alcuni di questi punti critici.

Un problema riguarda la ricerca della configurazione. Babel attualmente viene eseguito per file, quindi quando compila un file, cerca la configurazione più vicina (.babelrc) per sapere contro cosa compilare. Continua a cercare nell'albero delle directory se non la trova nella cartella corrente.

project
└── .babelrc // closest config for a.js
└── a.js
└── node_modules
└── package
└── .babelrc // closest config for b.js
└── b.js

Abbiamo apportato alcune modifiche:

  • Una è fermare la ricerca al confine del pacchetto (ci fermiamo quando troviamo un package.json). Questo assicura che Babel non tenti di caricare un file di configurazione al di fuori dell'applicazione, il caso più sorprendente è quando ne trova uno nella home directory.

  • Se utilizziamo un monorepo, potremmo voler avere un .babelrc per pacchetto che estende qualche altra configurazione centrale.

  • Babel stesso è un monorepo, quindi invece stiamo utilizzando il nuovo babel.config.js che ci permette di risolvere tutti i file rispetto a quella configurazione (nessuna ulteriore ricerca).

Compilazione selettiva con "overrides"

Abbiamo aggiunto un'opzione "overrides" che ci permette sostanzialmente di creare una nuova configurazione per qualsiasi insieme di percorsi di file.

Ciò consente ad ogni oggetto di configurazione di specificare un campo test/include/exclude, proprio come si farebbe per Webpack. Ogni campo può accettare un singolo elemento o un array di elementi che possono essere una string, un RegExp o una function.

Questo ci permette di avere una singola configurazione per tutta l'applicazione: potremmo voler compilare il codice JavaScript del server in modo diverso rispetto al codice client (oltre a compilare alcuni pacchetti in node_modules).

JavaScript
// 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" } },
}],
],
}],
}

Raccomandazioni da discutere

Dovremmo evolvere la nostra visione statica della pubblicazione JavaScript verso un approccio che tenga il passo con gli standard più recenti.

Dovremmo continuare a pubblicare ES5/CJS sotto main per compatibilità con gli strumenti esistenti, ma anche pubblicare una versione compilata con la sintassi più recente (senza proposte sperimentali) sotto una nuova chiave standardizzabile come main-es. (Non credo che module debba essere questa chiave poiché è concepita solo per i Moduli JavaScript).

Forse dovremmo definire un'altra chiave in package.json, ad esempio "es"? Mi ricorda il sondaggio che ho fatto per babel-preset-latest.

Compilare le dipendenze non è un vantaggio per un singolo progetto/azienda: richiede uno sforzo collettivo di tutta la comunità. Sebbene questo processo sia naturale, potrebbe richiedere una standardizzazione: possiamo definire criteri per come le librerie possono aderire alla pubblicazione ES2015+ e verificarli tramite CI/strumenti/npm stesso.

La documentazione deve essere aggiornata menzionando i vantaggi della compilazione di node_modules, come implementarla per gli autori di librerie e come utilizzarla nei bundler/compilatori.

Con Babel 7, gli utenti possono usare preset-env più sicuramente e attivare la compilazione di node_modules con nuove opzioni di configurazione come overrides.

Facciamolo!

Compilare JavaScript non dovrebbe limitarsi alla distinzione ES2015/ES5, sia per la nostra app che per le dipendenze! Spero che questo sia un incoraggiante invito all'azione per riavviare le discussioni sull'utilizzo delle dipendenze pubblicate in ES2015+ come componenti di prima classe.

Questo post esamina alcuni modi in cui Babel può supportare questo sforzo, ma avremo bisogno dell'aiuto di tutti per cambiare l'ecosistema: più formazione, più pacchetti pubblicati con opt-in e strumenti migliori.


Ringraziamenti alle molte persone che hanno revisionato questo post, tra cui @chrisdarroch, @existentialism, @mathias, @betaorbust, @_developit, @jdalton, @bonsaistudio.