Life was good, everything was going ok until one day I recieved a warning on one of my django projects: ...psst! While Bower is maintained, we recommend yarn and webpack for new front-end projects!

I thought it was going to be easy, just a couple of packages to install and that would be it, I took this as a chance to learn more about npm and webpack. But I underestimated JS again.

How it was before

I was using npm and bower to download all the assets I needed for my django project (jquery, bootstrap, fontawesome, some jquery plugins, etc.), those were downloaded on the bower_components folder. My package.json had a postinstall script that would do the work. I'm using heroku so that would be done automatically on each deployment.


{
  "name": "quizz",
  "version": "1.0.0",
  "description": "Quizz maker",
  "main": "index.js",
  "scripts": {
    "postinstall": "./node_modules/bower/bin/bower install"
  },
  "author": "Pablo Leano",
  "license": "ISC",
  "dependencies": {
    "bower": "^1.8.2"
  }
}

I use django-pipeline to group those assets and collect them for production. To do so, I needed the bower_components in the STATICFILES_DIRS variable in my settings.

But now bower is deprecated and I had a warning saying that it might not work one day. So I started contemplating the possible solutions.

Three solutions

1.- Use a CDN instead of downloading and serving the files myself

Probably the easiest solution, the only thing I need to do is search the urls and add them to my base template directly. The problem is that I couldn't find an url for one of my jquery plugins. I could maybe upload it to S3 or add it to my repository, I don't like that idea. Also, using a CDN means that I'd have to decentralize my assets. The best of using pipeline is that I can group my assets (JS, CSS files) into different groups and use them easily in my templates, then upgrading or modifying one of them is not a problem. An everything is defined in my settings file.

Pros

  • It's easy to do (hard to maintain).
  • I'll have less dependencies. I no longer need node at all

Cons

  • It's slow on development env. Sometimes I work with a 4G connection, and usually with the cache disabled.
  • It gets complicated if you can't find your plugin in a CDN.
  • I cannot bundle all the assets them into a single file (or multiple files).

2.- Use webpack like a big JS boy

Bower recommends yarn, but I already have npm and it works as expected so I don't see why should I change it. I have never used webpack so I started reading and testing it.

I changed my package.json file into something like this:


{
  "name": "quizz",
  "version": "1.0.0",
  "description": "Quizz maker",
  "main": "index.js",
  "author": "Pablo Leano",
  "license": "ISC",
  "dependencies": {
    "@fortawesome/fontawesome-free": "^5.4.2",
    "bootstrap": "^4.1.3",
    "jquery": "^3.3.1",
    "jquery-sortable": "^0.9.13",
    ... // other plugins
  },
  "devDependencies": {
    "node-sass": "^4.9.4",
    "file-loader": "^2.0.0",
    "sass-loader": "^7.1.0"
    "css-loader": "^1.0.1",
    "style-loader": "^0.23.1",
    "webpack": "^4.24.0",
    "webpack-cli": "^3.1.2"
  }
}  

Then I spent a lot of time trying to create my webpack.config.js that would bundle correctly everything (css files, font files and js files).


var webpack = require('webpack');
const path = require('path');

module.exports = {
  entry: './app.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
         test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
         use: [{
           loader: 'file-loader',
           options: {
             name: '[name].[ext]',
             outputPath: 'fonts/',    // where the fonts will go
             publicPath: '../'       // override the default path
           }
         }]
       }
    ]
  },
  plugins: [
      new webpack.ProvidePlugin({
         $: "jquery",
         jQuery: "jquery"
     })
  ]
};

And an entrypoint to import everything


// app.js

import 'jquery';
import 'bootstrap';
import '@fortawesome/fontawesome-free/js/all';
import 'jquery-sortable';
import 'bootstrap/dist/css/bootstrap.min.css';

After changing my django-pipeline configuration I was ready to test it and it was working great! I had a single file bundle.js that was injecting all the css needed on the page and the needed js files. But then I clicked on a link and changed the page; since the CSS (bootstrap) is being injected into the header of the page, when loading the page first you'll see the raw content for a brief period of time until bootstrap is loaded and then you'll see your page correctly displayed. It's ugly ugly ugly!

Possible solutions to that problem came to my mind: display a loader while bootstrap is getting ready. Or maybe see if webpack can just copy the damn css files and fonts to a different folder. In the end I realized that using webpack was an overkill, the only thing I wanted was something like bower, something to download the package into a folder that I could then use with django-pipeline.

Pros

  • It's what bower recommends.
  • I can learn something new
  • I can brag about my new aquired JS skills

Cons

  • A complete overkill.
  • Since CSS is injected by JS, it takes a visible small amount of time to load and the page blinks whenever you change pages.

It was nice though, but I think I will only use webpack on a SPA.

3.- Use npm and a small js script to copy them to a different folder

Bower used to download the packages and place them into the bower_components folder. Now I download them using npm and they are in the node_modules folder along with many other node packages. So all I need is a simple JS script to read my packages.json file, extract the dependencies and copy those folders into a assets folder that I will add to my STATICFILES_DIRS in my django settings.


// package.json
{
  "name": "quizz",
  "version": "1.0.0",
  "description": "Quizz maker",
  "main": "index.js",
  "scripts": {
    "postinstall": "node django-assets.js"
  },
  "author": "Pablo Leano",
  "license": "ISC",
  "dependencies": {
    "@fortawesome/fontawesome-free": "^5.4.2",
    "bootstrap": "^4.1.3",
    "jquery": "^3.3.1",
    "jquery-sortable": "^0.9.13",
    "popper.js": "^1.14.4"
  },
  "devDependencies": {
    "cpx": "^1.5.0"
  }
}

// django-assets.js

const util = require('util');
const cpx = require("cpx");
const fs = require('fs');

let rawdata = fs.readFileSync('package.json');  
let packageJson = JSON.parse(rawdata);

Object.keys(packageJson.dependencies).forEach(function (packageName){
  cpx.copy(
    util.format("node_modules/%s/**", packageName),
    util.format("assets/%s", packageName),
    {
      "update": true
    },
    function(err){
      if(err) {
        console.log(util.format("cpx error for %s: %s", packageName, err));
        process.exit(1);
      } else {
        console.log(util.format("copied %s to assets", packageName));
      }
    }
  );
});

Pros

  • It's something similar to what I had before with bower

Cons

  • It doesn't handle the requirements of the node packages, but it's ok.
  • JS devs gonna hate.

Conclusion

I'm staying with the last solution because it's the closest to what I had before, but I will certainly discuss it with my JS friends. They might have a better solution.