Imparare come configurare Webpack non è un percorso scontato; credo sia anzi necessario considerarlo come un percorso graduale: imparare tutto non ha senso ed è troppo complesso, pertanto considero saggio approfondire di volta in volta le configurazioni relative a problemi specifici.
Dopo aver perso il conto dei problemi specifici che ho affrontato, mi è capitato tra le mani il mio primo progetto Rails + Webpacker, con il task di fare una semplice implementazione frontend. Subito sono stato contento: ho scelto una libreria JS, immaginavo uno yarn add
, una configurazione e via.
Se non fosse che dentro Rails non si configura Webpack, ma Webpacker. L'esperienza è stata un po’ turpiloquio un po’ bella sorpresa.
Prima delle opinioni, verticalizziamo sull'esigenza e come si raggiunge l'obiettivo.
Configurations over conventions over configurations
Coerente con la filosofia Rails - ed in generale con quella Agile - anche Webpacker si affida al motto “convenzioni prima delle configurazioni”.
By default, you don’t need to make any changes to config/webpack/*.js files since it’s all standard production-ready configuration.
Ma sappiamo bene che arriva il momento di aggiungere configurazioni personalizzate alle convenzioni standardizzate. Webpacker di default non integra file-loader
. Leggiamo le istruzioni della libreria per capire come installarla da https://www.npmjs.com/package/file-loader.
- To begin, you’ll need to install
file-loader
:
1
$ npm install file-loader --save-dev
Prima questione facile e diffusa anche fuori dal contesto Rails+Webpacker: non dovremo usare npm
bensì yarn
; sono preparatissimo ed uso con decisione
1
yarn add file-loader --dev
- Then add the loader to your
webpack
config. For example:
webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {},
},
],
},
],
},
};
Noi non abbiamo il file webpack.config.js
(turpiloquio), ma per fortuna abbiamo della documentazione su come intervenire (bella sorpresa).
Ecco i passaggi per affrontare la nostra necessità.
Per quale ambiente aggiungiamo la configurazione?
Noi abbiamo una configurazione generale in config/webpack/environment.js
e delle configurazioni specifiche in config/webpack/xxx.js
dove xxx
è il nome di un nostro ambiente.
Tutte queste configurazioni seguono questa logica:
- in cima importano
environment
- in mezzo modificano - se necessario - delle chiavi di questa
const
- esportano
environment
1
2
3
const { environment } = require('@rails/webpacker')
// do something on environment
module.exports = environment
oppure i singoli ambienti importano environment
dalla configurazione principale piuttosto che dal node module e lo esportano “preparato” per Webpack:
1
2
3
4
5
process.env.NODE_ENV = process.env.NODE_ENV || 'production' // <-- set env
const environment = require('./environment') // <-- generic conf
// do something on environment
module.exports = environment.toWebpackConfig()
Noi interverremo orizzontalmente su tutti gli ambienti e quindi prenderemo in considerazione config/webpack/environment.js
Aggiungere un loader
1
2
3
4
5
6
7
const { environment } = require('@rails/webpacker')
const fileLoader = require('./loaders/file')
// do something on environment
environment.loaders.append({key: 'file', value: fileLoader})
module.exports = environment
dove questo è il file loaders/file.js
1
2
3
4
5
6
7
8
9
module.exports = {
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {},
},
]
}
Perché ho scelto di scrivere questo file? Solo per pulizia. Non c'è altro motivo.
A questo punto demistifichiamo la prima magia e per farlo facciamo un atto di fede su cosa sia environment.loaders
e di quali funzioni abbia a disposizione. Intuiamo che append
aggiungerà al fondo “qualcosa” così come noi stiamo richiedendo con l'argomento
1
{key: 'file', value: fileLoader}
Partiamo dall'argomento:
key
è un nome arbitrario. Quando diciamo arbitrario ovviamente sottintendiamo la responsabilità di dare un nome comprensibile e riconoscibile e riconducibile.value
invece è un oggetto che corrisponde ad una singola regola dell'arrayrules
così come ci viene dato dalla documentazione originale dei pacchetti NPM.
Rivediamoli entrambi vicini per capire meglio l'oggetto che ci torna utile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
module: {
rules: [
// start
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {},
},
],
},
// end
],
},
};
1
2
3
4
5
6
7
8
9
module.exports = { //start
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {},
},
]
} //end
Quindi a questo punto abbiamo chiaro cosa possiamo copincollare dalla documentazione di file-loader
: sappiamo trovare lo snippet corretto e sappiamo dove metterlo. Inoltre il trucco di isolare lo snippet in un file autonomo ci permette di riconoscerlo a colpo d'occhio, riutilizzarlo in diversi ambienti, fare commit atomici per le singole configurazioni.
Ok, ma: perché e come?
Perché?
Imparare questa cosa mi ha fatto sentire in primo luogo rabbia e disagio. Dopo tutto lo sforzo che ho fatto in passato per imparare a configurare Webpack (sfogo: e re-impararlo ad ogni aggiornamento sconvolgente la struttura del suo webpack.config.js
), l'impressione è quella che in questo contesto la mia esperienza servisse a poco.
Incredulo ed in assetto berserker ho deciso di verificare il fatto che tutto questo layering non servisse solo ed esclusivamente per accomodare i developer Rails fornendo loro qualche configurazione generica in un formato a loro più familiare: config/webpacker.yml
.
Il tour è cominciato guardando che cosa ci portiamo in pancia nella nostra configurazione javascript nel momento in cui importiamo const { environment } = require('@rails/webpacker')
. Cosa fa Webpakcer quando gli chiediamo environment
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// [...]
const createEnvironment = () => {
const path = resolve(__dirname, 'environments', `${nodeEnv}.js`)
// Se nodeEnv non è settato, Environment farà un po' di magia scegliendone
// uno e dandoci una mano
const constructor = existsSync(path) ? require(path) : Environment
return new constructor()
}
module.exports = {
// [...]
environment: createEnvironment(), // <--- Il nostro environment
// [...]
}
Dunque per un ambiente production
otterremo come environment
un'istanza di un oggetto come questo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// [...]
module.exports = class extends Base {
constructor() {
super()
this.plugins.append(
// [...]
)
this.plugins.append(
// [...]
)
this.config.merge({
// [...]
})
}
}
Dove ovviamente Base
è un oggetto con le configurazioni comuni (non lo riporto perché è troppo lungo, ma lo si può leggere qui. Di fatto abbiamo un modo complicato, ma “privato”, di costruire oggetti (Builder pattern), là dove un webpack.config.js
- per quanto perverso - è un plain object da scrivere.
La risposta alla mia domanda iniziale è dunque: non solo. Sì: una fetta del codice è dedita a prendere RAILS_ENV
e capire se possiamo usare quello se non abbiamo un NODE_ENV
, un'altra fetta è dedita a leggere le configurazioni in YAML. Ma ci sono due distinte note da fare a mio parere:
- il file YML è davvero solo un palliativo per rubysti senza competenze di Webpack nello specifico
- Tutto il resto del codice è un’elegantissima soluzione per la gestione degli environment. Chiunque si sia scritto a mano una soluzione per lo stesso scopo sa bene quanto possa essere complicato e quanto manutenerla all'interno del codice sia ostico per chi non ha le conoscenze giuste. Ed alla fine, all'interno dello scontro perpetuo del rapporto qualità/tempo, integrare queste cose by project produce soluzioni claudicanti.
Come?
Al netto di tutto quello che abbiamo visto, rimane ancora almeno una magia da demistificare: environment.loaders.append
. Io sono abituato a scrivere configurazioni Webpack con chili di righe di plain objects; questo append
cosa sta facendo?
In realtà per capirlo dobbiamo guardare che cosa sia loaders
. Un trucco molto povero e molto efficace è di scrivere questo in config/webpack/environment.js
:
1
2
3
4
5
6
7
8
const { environment } = require('@rails/webpacker')
const resolveConfig = require('./resolve/alias')
environment.loaders.add({key: 'file', value: fileLoader})
console.log(environment.loaders) // <--- \m/ O_O \m/
module.exports = environment
e poi chiamare da linea di comando la compilazione con ./bin/webpack
.
Dovremmo vedere un output del tutto simile a questo nella console:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ConfigList [
{ key: 'babel',
value:
{ test: /\.(js|jsx)?(\.erb)?$/,
exclude: /node_modules/,
use: [Array] } },
{ key: 'css',
value:
{ test: /\.(css)$/i, use: [Array], exclude: /\.module\.[a-z]+$/ } },
{ key: 'sass',
value:
{ test: /\.(scss|sass)$/i,
use: [Array],
exclude: /\.module\.[a-z]+$/ } },
{ key: 'moduleCss',
value:
{ test: /\.(css)$/i, use: [Array], include: /\.module\.[a-z]+$/ } },
{ key: 'moduleSass',
value:
{ test: /\.(scss|sass)$/i,
use: [Array],
include: /\.module\.[a-z]+$/ } },
{ key: 'file',
value:
{ test: /\.(png|jpg|gif)$/,
use: [Array] } } ]
A proposito della definizione “elegantissima” che ho usato prima: che cosa è una ConfigList
? Possiamo vederlo su GitHub.
Detto in due righe ConfigList
è un oggetto che estende Array
restituendo oggetti di prototipo Array
aumentati come se la standard library di JS fosse una cosa seria.
Tra le altre una semplice funzione values()
1
2
3
values() {
return this.map(item => item.value)
}
Dopodiché environment
ha una funzione toWebpackConfig()
che, per quel che riguarda i loaders, fa la cosa più semplice del mondo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
toWebpackConfig() {
return this.config.merge({
// [...]
module: {
// [...]
rules: [
...this.loaders.values()
]
},
// [...]
})
}
ricostruendo la struttura dell'oggetto di configurazione di Webpack così come tutti (?) lo conosciamo!
Non so voi, ma io tolta la magia mi sento molto meglio 😁