16 October 2022

I have been a long-time fan of what once was called Pika and now is called Snowpack. Basically, it was the revolution how JavaScript web-apps are built. Instead of requiring a custom dev-server and doing a lot of "bundler magic" behind the scenes (basically every framework out there like Angular, Vue, etc. using Webpack), it just processed the Node dependencies and converted them into standard ES6 modules. What you could do now is reference this standard ES6 modules from your App without the need of a special build step on your application or custom dev-server. Modern browser can process imports like import { html, LitElement } from './lib/lit-element.js';. Just copy your HTML, standard/vanilla JS on a plain web-server (or use a generic tool like browser-sync) and way you go. You can read more about the general approach in one of my previous posts.

To me this approach always felt very natural, intuitive and did not introduce too much dependency on complex tools that lock you in. With Snowpack 3, I am getting the same vibe now like previously with Webpack. It has become a complex tool (includes bundeling, minification, etc.) that requires you to now use it’s own dev-server.

For this reason, I have now moved back to a lower-level tool which is called rollup.js. With rollup.js, we can convert Node dependency into standard ES6 modules. Nothing more and nothing less. You can find the full example project on GitHub.

The main parts are the package.json with dependecy to rollup and the webDependencies section that I have kept analogous to how Pika/Snowpack have it:

{
  "name": "webstandards-starter",
  "version": "1.0.0",
  "description": "Starter project for web-development using the web's latest standards.",
  "main": "src/AppMain.js",
  "scripts": {
    "postinstall": "rollup -c", (1)
    "start": "browser-sync src -f src --single --cors --no-notify --single"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/38leinaD/webstandards-starter.git"
  },
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/38leinaD/webstandards-starter/issues"
  },
  "homepage": "https://github.com/38leinaD/webstandards-starter#readme",
  "devDependencies": {
    "browser-sync": "^2.27.10",
    "rollup": "^3.2.1", (2)
    "@rollup/plugin-node-resolve": "^15.0.0"
  },
  "dependencies": {
    "@vaadin/router": "^1.7.4",
    "lit-element": "^3.2.2"
  },
  "rollup": {
    "webDependencies": [ (3)
      "@vaadin/router/dist/vaadin-router.js",
      "lit-element/lit-element.js",
      "lit-html/directives/async-append.js",
      "lit-html/directives/async-replace.js",
      "lit-html/directives/cache.js",
      "lit-html/directives/class-map.js",
      "lit-html/directives/guard.js",
      "lit-html/directives/if-defined.js",
      "lit-html/directives/repeat.js",
      "lit-html/directives/style-map.js",
      "lit-html/directives/unsafe-html.js",
      "lit-html/directives/until.js"
    ]
  }
}
  1. postinstall runs rollup when executing npm install

  2. devDependency to rollup and rollup plugin

  3. Similar webDependencies configuration as known from Pika/Snowpack

You can see that I added a postinstall step executing rollup -c. What this will do is call rollup on npm install and use the rollup.config.mjs file which looks like this:

import { nodeResolve} from '@rollup/plugin-node-resolve';
import * as fs from 'fs';
import * as path from 'path';

function outDir(relPath) {
  const nodeModulesPath = `./node_modules/${relPath}`
  const parentDir = path.dirname(relPath)

  // Just some basic logic how to generated output-paths under src/lib
  if (`${path.basename(parentDir)}.js` === path.basename(relPath)) {
    // lit-element/lit-element.js is simplified to 'src/lib/lit-element.js'
    return path.dirname(parentDir)
  }
  else {
    return path.dirname(relPath)
  }
}

export default JSON.parse(fs.readFileSync('package.json', 'utf8')).rollup.webDependencies.map(relPath => {
  console.log("Processing:", relPath)

  const nodeModulesPath = `./node_modules/${relPath}`

  return {
    input: [
      nodeModulesPath
    ],
    output: {
      dir: 'src/lib/' + outDir(relPath),
      format: 'esm',
    },
    plugins: [nodeResolve({
      browser: true
    })]
  };
});

What this does is the bare minimum of what Pika and Snowpack are also doing: Process each of the elements in webDependencies and convert the dependency into a standard ES6 module. The ES6 module is created under src/lib and allows for easy referencing via import from the application. After running the install-step, you can copy the app to any standard web-server; or use browser-sync for that matter.

I am not saying that this is the way to go for bigger commerical projects, but to me this makes for a simple and understandable setup that at least serves me well for learning purposes and personal projects. Eventually, most libraries/dependencies will come out of the box as modules and the rollup step can be eliminated completely.