sviluppiamo comunicazione con weLaika Advertising

Demistificare la configurazione di Webpacker

Tags: ruby, rails, wepack, webpacker, loaders, config
Alessandro Fazzi -
Alessandrofazzi

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:

  1. in cima importano environment
  2. in mezzo modificano - se necessario - delle chiavi di questa const
  3. 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'array rules 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:

  1. il file YML è davvero solo un palliativo per rubysti senza competenze di Webpack nello specifico
  2. 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 😁