If you want to install static JS libraries in your project and you know Bower is no longer an option. I also tried Webpack and did not like it for my use case because it was too complex for what I was building. Therefore, being very familiar with nodejs, npm and plain-js, I wrote a script that is simple and “extensible”.

It may take some time to do it, but you only have to do it once.

Context

I was developing a bit of JS for a traditional website: a backend in Python 3, running in PyPy in production for speed; some templates written in Jinja2 and some JS to make everything more interactive: jquery, bootstrap, datatables, etc. Initially all my JS dependencies were statically linked to cdn routes, but I soon realised that renovate is never going to pick up these dependencies and make pull requests with updates.

Initially I looked into Bower, but unfortunately after a long debate, the project has been deprecated in favour of more snappy tools, such as npm or yarn. Plus, package.json is a more complete package format. Unfortunately, I did not find an easy replacement for my use case, where I basically want to have an updating package format, but all my dependencies statically linked to the web page. Basically, this is one use-case where bower was doing the work for the developer by default and it’s not really covered by other tools.

Attempt 1: npm + webpack

A medium post recommends using npm and webpack. So I tried this approach. I first updated webpack to the latest version, as the post suggests a much older version. Then wrote my script with the Webpack Copy plugin and it was working, but not as I expected.

The main problem was that webpack usually creates bundles. So it expects and entry point in their build process, which is explored for all the dependencies, then tree shaking is performed and the bundle is built. If you want a single vendor bundle with all you js, then webpack if the way to go. In my case I did not want that and it seemed a bit tricky to create a fake entry point just to copy some assets.

Attempt 2: bower’s migration guide

Bower made a few recommendation on how to migrate your project to yarn when the tool was being deprecated. But since I already learned my lesson with the webpack attempt, I was not convinced that having some dev dependencies added just to copy some resources would solve my problems.

Solution

My requirements for this script are pretty clear:

  • I want renovate to be able to update the dependencies -> package.json is well supported
  • Easy to understand
  • Make it fool proof: the script should not allow me to miss a step -> postinstall target in package.json
  • Ability to delete node_modules after all this is done (because this take space)
  • In case the structure of the package changes or they’re no longer publishing a bundle the script should fail

Here is the package.json file:

{
  "name": "ui-js-dependencies",
  "author": "Ovidiu",
  "dependencies": {
    "@fortawesome/fontawesome-free": "^5.7.2",
    "Select2": "^3.5.7",
    "bootbox": "^4.4.0",
    "bootstrap": "^4.3.1",
    "datatables.net": "^1.10.19",
    "datatables.net-bs4": "^1.10.19",
    "filepond": "^4.2.0",
    "jquery": "^3.3.1",
    "underscore": "^1.9.1"
  },
  "scripts": {
    "postinstall": "npm run move-assets",
    "move-assets": "node ./move-assets.js"
  },
  "devDependencies": {
    "fs-extra": "^7.0.1"
  }
}

And move-assets.js:

const path = require('path');
const fs = require('fs-extra');
const _ = require('underscore');

const TARGETS = [
    './static/vendors/js',
    './static/vendors/css',
    './static/vendors/css/img',
    './static/vendors/webfonts'
];

// (source, target) pairs. This allows you to rename assets as well (e.g. fontawesome)
const ASSETS = {
    '@fortawesome/fontawesome-free/js/all.min.js': 'js/fontawesome-free.all.min.js',
    '@fortawesome/fontawesome-free/css/all.min.css': 'css/fontawesome-free.all.min.css',
    'jquery/dist/jquery.min.js': 'js/jquery.min.js',
    'datatables.net/js/jquery.dataTables.min.js': 'js/jquery.dataTables.min.js',
    'datatables.net-bs4/js/dataTables.bootstrap4.min.js': 'js/dataTables.bootstrap4.min.js',
    'datatables.net-bs4/css/dataTables.bootstrap4.min.css': 'css/dataTables.bootstrap4.min.css',
    'underscore/underscore-min.js': 'js/underscore.min.js',
    'bootstrap/dist/js/bootstrap.bundle.min.js': 'js/bootstrap.bundle.min.js',
    'bootstrap/dist/css/bootstrap.min.css': 'css/bootstrap.min.css',
    'Select2/select2.min.js': 'js/select2.min.js',
    'Select2/select2.css': 'css/select2.css',
    'bootbox/bootbox.min.js': 'js/bootbox.min.js'
};

// make sure that all targets exist
TARGETS.forEach((target) => fs.ensureDirSync(path.resolve(__dirname, target), { recursive: true }))

// copy each asset
_.each(ASSETS, (target, source) => fs.copyFileSync(path.resolve(__dirname, `./node_modules/${source}`),
    path.resolve(__dirname, `./static/vendors/${target}`)));

// special packages - copy some assets recursively
fs.copySync(path.resolve(__dirname, `./node_modules/@fortawesome/fontawesome-free/webfonts/`),
    path.resolve(__dirname, `./static/vendors/webfonts/`));   

It takes a bit of time to explore all your assets and find the files you want to copy, but once you’ve done that everything is fine. You’ll only have to do it again when you install new packages.

How does it work?

Basically, each time you do an:

npm install

the move-assets target is executed. You can also execute that manually.

Note: If you are running this in docker, most node images disable postinstall targets by default. You can either enable them or :

 npm install && npm run move-assets

The move-assets.js script ensures that the targets exists, in this case everything is copied in static/vendors/.

When you install a new package that exposes a few assets (js, css, fonts, images), you can have a look at the package (and this is generally a good idea, because it forces you to understand their structure in case something breaks) and add the assets to the ASSETS list. You can split that list into multiple lists based on type (e.g. ASSETS_CSS, ASSETS_JS, etc). If you want to copy an asset folder recursively (e.g. fonts) then you can do that manually as a special case.

The most important thing: if the structure of a package changes during a version update, the copy step will fail.