NOTE: Snowpack is obsolete at this point, which makes this post, um, less useful.


If you've been working with JavaScript for a while, you'll know that things have changed from the day when the code end users saw in production was the same code you hacked with your hands. Though I might prefer the days of Vanilla.js, it's gotten nearly impossible to hold a job without knowing how to transpile code.

If you're still on the Vanilla side of the chart, transpiling JavaScript code is when you take a recent version of JavaScript, or, more properly, ECMAscript, and essentially compile it into an older, more compatible version. Transpiling is a "source-to-source" compilation, from one programming language (or version) to another, so to speak.

Maybe you want to use "farts" or "fat arrow functions" where you can exchange this code...

function (x) {
    return x + 1;
}

... with the shorthand...

(x) => x + 1;

Or maybe you want to use shorthand property definitions, which turns this:

var o = {
  a: a,
  b: b,
  c: c
};

... into...

let o = {a, b, c};

Or maybe you just want to use let and const instead of being limited to var.

Or, even better, maybe you want to use TypeScript, which is a great idea, bringing along the bug-squashing safety of strong-typing without sacrificing the abilty to go full dynamic when you need it.

You get the point. JavaScript keeps adding features (as does TypeScript), but older browsers won't magically support all of them. You need to turn your cutting-edge code into something [insert lowest common denominator based on your browser requirements] can run.

That's transpilation. Even if you're not going to transpile something, you need to be aware of how it works so that you can pick the right tools for your projects.

I'll probably make a video at some point, but I would like to quickly get down the steps I think I'd take when moving from a vanilla, es5-compatible JavaScript codebase.


Notes, warnings, and caveats

Why target your transpilation for es5? es5 is the latest version IE supports, as a benchmark. es6 requires more modern browsers. Luckily requirements to support IE seem to be going the way of the dinosaur.

That said, I haven't (yet) run into a situation where a transpiler balks at targeting es5. That is, why not include IE if transpiling to something newer buys me nothing? Everything I can do in es6 I can, thanks to some insane transpiler shimming, do in es5.

Warning: This is not going to be a howto, I'm afraid, but a command recipe for those who already kind of know what they're doing.

It's also not a really nuanced recipe. For instance, when you run npm init, you probably don't want to use the -y flag and should instead pick a specific license. For some reason, using the -y flag means you'll pick "ISC", "a permissive free software license published by the Internet Software Consortium" instead of "UNLICENSED" for your project, and "UNLICENSED" is what the docs say to use for copyrighted code.

So use at your own risk.

And to be overly clear, this is for browser development, not server-side node dev, natch.


Recipe to move es5 codebase to modern JS (with snowpack, webpack, & babel)

Here's the 10,000' view:

  • Use Snowpack to transpile your TypeScript into modern JavaScript.
  • Use webpack to fold your modern JavaScript into a single file.
  • Use babel to transpile your modern JavaScript into es5 compliant code.
  • (Use some node code, called by your build process, to link to that single es5 page from your starting html.)

Here are the specific steps:

  1. Ensure you've got node installed globally
    • The node install will include installing npm.
    • If you know you will need to swap to different versions of node for different projects (as in you know you have some older ones that won't work in the latest version of node), google nvm ("node version manager").
      • You should, however, know if this applies to you.
      • (If this is your first transpilation project, different versions of node don't apply. Yet.)
  2. Navigate to an es5 code base.
  3. In the home directory of the codebase, run npm i -y
    • See caveat about licensing, above.
  4. Now run npm install -D snowpack
    • Snowpack is a development tool only imo.
    • It is not a conventional transpiler so much as a package manager.
    • snowpack's goal is to allow you to develop in a browser that supports whatever version of JavaScript you want to program in (vs. deploy) quickly.
    • It does, however, support compiling TypeScript on the fly. That and the more deliberate management of libraries are the biggest gains snowpack buys for me.
  5. "Port" your existing es5 to TypeScript by changing all JavaScript files' extensions from .js to .ts
    • TypeScript is a superscript of JavaScript, so that's all you have to do.
    • On macOS, you can use this command:
      • find . -iname "*.js" -exec bash -c 'mv "$0" "${0%\.js}.ts"' {} \;
    • I've got a PowerShell script to do this somewhere, but until I find it, try these answers.
  6. Add a snowpack.config.js file to the root directory.
  7. Add "start": "snowpack dev" to your package.json's scripts collection
  8. Install webpack as a "dev dependency" for your project with npm install -D webpack-cli
  9. Add this build command to your package.json's scripts collection:
    • "build": "snowpack build && webpack"
    • Here, snowpack is going to compile your TypeScript into JavaScript.
    • Then webpack is going to take the compiled JavaScript and pack it into a single index.js file.
    • Hang in there. We're getting to the transpilation.
  10. npm install --save-dev babel-loader @babel/core
    • We're using Babel to transpile the code.
    • Up until this point, we needed a modern browser that understood our untranspiled code.
    • Not a big deal as you develop, but potentially a very big deal before releasing.
  11. Add this command to package.json:
    • "launderIndexHtml": "node ./buildscripts/build.js"
  12. Add build.js to your project's home directory using the template, below.
  13. Edit package.json's build command to include this new build command:
    • "build": "snowpack build && webpack && npm run launderIndexHtml",
    • Why didn't we just start with that for our build command? Idk. This recipe is too tutorial-ly, I suppose.

Suggested initial snowpack.config.js

/*global module */
module.exports = {
    mount: {
        public: { url: "/", static: true },
        app: "/",
    },
    buildOptions: {
        out: "./dist",
    },
    devOptions: {
        open: "brave",
    },
};

Why does this open Brave when in devOptions.open? Well, I like to have a browser dedicated to testing and Canary wasn't an option snowpack supported at the time of this writing, strangely enough.


Suggested initial webpack.config.js

/*eslint-disable */
const path = require("path");
module.exports = {
    entry: "./dist/index.js",
    output: {
        filename: "./index.js",
        path: path.resolve(__dirname, "dist"),
    },
    module: {
        rules: [
            {
                test: /\.(js)$/,
                exclude: /node_modules/,
                use: ["babel-loader"],
            },
        ],
    },
    resolve: {
        extensions: ["*", ".js"],
    },
};

Custom build.js script

/* eslint-env node */
// We need to do two things after we've compiled our TypeScript with snowpack,
// transpiled into es5 with babel, and bundled & minified with webpack:
// 1. Update index.html to load our minified es5 file as-is, not as a module.
// 2. Get rid of the source we bundled into our webpack output.
var fs = require("fs");
var indexLoc = "./dist/index.html";
fs.readFile(indexLoc, "utf8", function (err, data) {
    if (err) {
        return console.log(err);
    }
    var result = data.replace('script type="module" src="./index.js"', 'script src="./index.js"');
    // var result = data.replace(/script type="module" src="./index.js"/, 'script src="./index.js"');
    fs.writeFile(indexLoc, result, "utf8", function (errWrite) {
        if (errWrite) {
            return console.log(errWrite);
        }
    });
});
function getDirectories(path) {
    return fs.readdirSync(path).filter(function (file) {
        return fs.statSync(path + "/" + file).isDirectory();
    });
}
var foldersToKeep = ["lib", "css", "assets"];
getDirectories("./dist").forEach((x) => {
    if (foldersToKeep.indexOf(x) === -1) {
        fs.rmdirSync("./dist/" + x, { recursive: true });
    }
});

Labels: , , ,