Compiling and bundling JavaScript, the painless way

Michele Riva

Michele Riva

Senior Software Architect @NearForm

Google Developer Expert

Microsoft MVP

MicheleRivaCode

Real-World Next.js

Build scalable, high performances and modern web applications using Next.js, the React framework for production

MicheleRivaCode

Compiling and bundling JavaScript

is (often) a pain*

*but it shouldn't

MicheleRivaCode

A bit of terminology: compiling

to change a computer program into a machine language

Cambridge Dictionary

MicheleRivaCode

A bit of terminology: compiling transpiling

to translate a source code into a different language  source code

Me, myself

MicheleRivaCode

A bit of terminology: bundling

to encapsulate code and resources into a single executable file

Me, myself

MicheleRivaCode

Why do we want to transpile our code?

  • To make it compatible with different platforms
  • To write our scripts in different languages
  • To adopt new language features
  • To run large-scale refactors

MicheleRivaCode

MicheleRivaCode

Scala.js

https://www.scala-js.org

MicheleRivaCode

ReasonML

ReScript

F# (via Fable)

Gleam

Elm

Kotlin

Nim, Haxe, C/C++ (via Emscripten), ClojureScript, Dart, PureScript, Haskell (via GHCJS)

MicheleRivaCode

MicheleRivaCode

LLVM

MicheleRivaCode

function toUpper(x) {
  return x.toUpperCase();
}

function addExclamationMarks(x) {
  return x + '!!!'
}

function scream(input) {
  return input
    |> toUpper
    |> addExclamationMarks
    |> console.log
}

scream('Hello, Serbia');
// HELLO, SERBIA!!!
"use strict";

function toUpper(x) {
  return x.toUpperCase();
}

function addExclamationMarks(x) {
  return x + '!!!';
}

function scream(input) {
  var _ref, _ref2, _input;

  return _ref = (
    _ref2 = (_input = input, toUpper(_input)),
    addExclamationMarks(_ref2)
  ), console.log(_ref);
}

scream('Hello, Serbia');
// HELLO, SERBIA!!!

MicheleRivaCode

import { VFC } from 'react';

enum UserType {
  ADMIN     = "admin",
  EDITOR    = "editor",
  USER      = "user",
  ANONYMOUS = "guest"
}

type MyProps = {
  userType: UserType;
}

const MyComponent: VFC<MyProps> = ({ userType }) => {
  return (
    <div>
      User is of type: {props.userType}
    </div>
  )
}
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
var UserType;

(function (UserType) {
  UserType["ADMIN"] = "admin";
  UserType["EDITOR"] = "editor";
  UserType["USER"] = "user";
  UserType["ANONYMOUS"] = "guest";
})(UserType || (UserType = {}));

const MyComponent = ({ userType }) => {
  return /*#__PURE__*/ React.createElement(
    "div",
    null,
    "User is of type: ",
    props.userType
  );
};

MicheleRivaCode

Why do we want to bundle our code?

  • To create a single executable file
  • To serve a single JS file over the net

MicheleRivaCode

MicheleRivaCode

ReadArticle.jsx

NewArticle.jsx

AuthorProfile.jsx

BundledPage.js

MicheleRivaCode

in depths

transpiling

MicheleRivaCode

Language-to-language

ClojureScript

(defn simple-component []
  [:div
   [:p "I am a component!"]
   [:p.someclass
    "I have " [:strong "bold"]
    [:span {:style {:color "red"}} " and red "] "text."]])
cljs.user.simple_component = (function cljs$user$simple_component(){
  return new cljs.core.PersistentVector(null, 3, 5, cljs.core.PersistentVector.EMPTY_NODE,
  [new cljs.core.Keyword(null,"div","div",(1057191632)),
  new cljs.core.PersistentVector(null, 2, 5, cljs.core.PersistentVector.EMPTY_NODE,
  [new cljs.core.Keyword(null,"p","p",(151049309)),"I am a component!"], null),
  new cljs.core.PersistentVector(null, 5, 5, cljs.core.PersistentVector.EMPTY_NODE,
  [new cljs.core.Keyword(null,"p.someclass","p.someclass",(-1904646929)),
  "I have ",new cljs.core.PersistentVector(null, 2, 5, cljs.core.PersistentVector.EMPTY_NODE, 
  [new cljs.core.Keyword(null,"strong","strong",(269529000)),"bold"], null),
  new cljs.core.PersistentVector(null, 3, 5, cljs.core.PersistentVector.EMPTY_NODE, 
  [new cljs.core.Keyword(null,"span","span",(1394872991)),
  new cljs.core.PersistentArrayMap(null, 1, [new cljs.core.Keyword(null,"style","style",(-496642736)),
  new cljs.core.PersistentArrayMap(null, 1, [
  new cljs.core.Keyword(null,"color","color",(1011675173)),"red"]
  , null)], null)," and red "], null),"text."], null)], null);
});

https://reagent-project.github.io

MicheleRivaCode

[@bs.config {jsx: 3}];

module Greeting = {
  [@react.component]
  let make = () => {
    <button> {React.string("Hello!")} </button>
  };
};

ReactDOMRe.renderToElementWithId(<Greeting />, "preview");
// Generated by BUCKLESCRIPT, PLEASE EDIT WITH CARE
'use strict';

var React = require("react");
var ReactDOMRe = require("./stdlib/reactDOMRe.js");

function _none_$Greeting(Props) {
  return React.createElement("button", undefined, "Hello!");
}

var Greeting = {
  make: _none_$Greeting
};

ReactDOMRe.renderToElementWithId(React.createElement(_none_$Greeting, { }), "preview");

exports.Greeting = Greeting;
/*  Not a pure module */

https://reasonml.github.io

Language-to-language

ReasonML

MicheleRivaCode

Language-to-language

TypeScript

import { VFC } from 'react';

enum UserType {
  ADMIN     = "admin",
  EDITOR    = "editor",
  USER      = "user",
  ANONYMOUS = "guest"
}

type MyProps = {
  userType: UserType;
}

const MyComponent: VFC<MyProps> = ({ userType }) => {
  return (
    <div>
      User is of type: {props.userType}
    </div>
  )
}
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
var UserType;

(function (UserType) {
  UserType["ADMIN"] = "admin";
  UserType["EDITOR"] = "editor";
  UserType["USER"] = "user";
  UserType["ANONYMOUS"] = "guest";
})(UserType || (UserType = {}));

const MyComponent = ({ userType }) => {
  return /*#__PURE__*/ React.createElement(
    "div",
    null,
    "User is of type: ",
    props.userType
  );
};

MicheleRivaCode

Every language has its own transpiler

TypeScript TSC

Fable

ClojureScript

BuckleScript

JavaScript (Babel)

MicheleRivaCode

Problem #1

transpilation time

Really fast

Average

Fast

Slow on large codebases

Slow on large codebases

MicheleRivaCode

Problem #2

optimized output

Beautifully optimized

Awful

Beautifully optimized

Quite optimized

Well optimized*

MicheleRivaCode

Let's focus on the most popular ones

Quite slow, quite optimized

Quite fast, well optimized*

MicheleRivaCode

Bundling time grows

Quite slow, quite optimized

Quite fast, well optimized

MicheleRivaCode

MicheleRivaCode

How does a transpiler work?

MicheleRivaCode

Parsing

Transformation

Codegen

MicheleRivaCode

Parsing

Step 1

Tokenization

var foo = 10
var
foo
=
10

input

tokens

MicheleRivaCode

Parsing

Step 2

Syntactic Analysis

var foo = 10
            =
           / \
          /   \
        var   10
         |
        foo

input

parse tree
(aka concrete syntax tree)

MicheleRivaCode

Parsing

Step 2

Syntactic Analysis

var foo = 10
            =
           / \
          /   \
        var   10
         |
        foo

input

       variableDeclaration
               |
               |
       vairiableDeclarator
          /         \
         /           \
   Identifier   NumericLiteral

abstract syntax tree (AST)

MicheleRivaCode

MicheleRivaCode

{
   "body":[
      {
         "type":"VariableDeclaration",
         "declarations":[
            {
               "type":"VariableDeclarator",
               "id":{
                  "type":"Identifier",
                  "name":"foo",
                  "loc":{
                     "identifierName":"foo"
                  }
               },
               "init":{
                  "type":"NumericLiteral",
                  "extra":{
                     "rawValue":10,
                     "raw":"10"
                  },
                  "value":10
               }
            }
         ],
         "kind":"var"
      }
   ]
}

MicheleRivaCode

MicheleRivaCode

export default function () {
  return {
    visitor: {
      VariableDeclaration(path) {
        console.log({ VariableDeclaration: path.node });
        // { type: "VariableDeclaration", kind: "var", ... }
      },
      Identifier(path) {
        console.log({ Identifier: path.node });
        // { type: "Identifier", name: "foo", ... }
      },
      NumericLiteral(path) {
        console.log({ NumericLiteral: path.node });
        // { type: "NumericLiteral", value: 10, ... }
      }
    }
  };
}

Traversing the AST

Babel example implementing the visitor pattern

MicheleRivaCode

export default function() {  
  return {
    visitor: {
      VariableDeclaration(path) {
        if (path.node.kind === "var") {
          path.node.kind = "let"
        }
      }
    }
  };
}

Transforming

Babel example

MicheleRivaCode

Codegen

Babel example

input

output

var foo = 10
const bar = true
let foo = 10;
const bar = true;

MicheleRivaCode

ESLint

jscodeshift

Prettier

MicheleRivaCode

ESLint

export default function(context) {
  return {
    TemplateLiteral(node) {
      context.report({
        node,
        message: 'Do not use template literals',

        fix(fixer) {
          if (node.expressions.length) {
            // Can't auto-fix template literal with expressions
            return;
          }
          
          return [
            fixer.replaceTextRange([node.start, node.start + 1], '"'),
            fixer.replaceTextRange([node.end - 1, node.end], '"'),
          ];
        },
      });
    }
  };
};

Example code taken from astexplorer.net

MicheleRivaCode

ESLint

const myString = `Hello, World!`;
const myString = "Hello, World!";

input

output

MicheleRivaCode

ttypescript (https://github.com/cevek/ttypescript)
export default function (program) {
  const checker = program.getTypeChecker();
  return (context) => {
    return (sourceFile) => {
      const visitor = (node) => {
        // This branch evaluates '2 + 2' like expressions and replaces the node with the result (in this case '4')
        if (ts.isBinaryExpression(node)) {
          if (ts.isNumericLiteral(node.left) && ts.isNumericLiteral(node.right)) {
            // We could parse `node.text` as a number, or we can use the typechecker to get type info for nodes
            const lhs = checker.getTypeAtLocation(node.left);
            const rhs = checker.getTypeAtLocation(node.right);

            switch (node.operatorToken.kind) {
              case ts.SyntaxKind.PlusToken:
                return context.factory.createNumericLiteral(lhs.value + rhs.value);
            }
          }
        }
        // 
        if (ts.isIdentifier(node) && node.text === 'printTips' || node.text === 'tips') {
            return context.factory.createIdentifier(node.text.split('').reverse().join(''));
        }
        return ts.visitEachChild(node, visitor, context);
      };
      return ts.visitNode(sourceFile, visitor);
    };
  };
};

Example code taken from https://github.com/cevek/ttypescript#program

MicheleRivaCode

ttypescript (https://github.com/cevek/ttypescript)

input

output

const mySum = 10 + 20;
const mySum = 30;

MicheleRivaCode

export const parser = "babel";

export default function transformer(file, api) {
  const j = api.jscodeshift;

  return j(file.source)
    .find(j.Identifier)
    .forEach((path) => {
      if (j(path).get().value.name.includes("oldName")) {
        j(path).replaceWith(
          j.identifier(path.node.name.replace("oldName", "newName"))
        );
      }
    })
    .toSource();
}
jscodeshift

MicheleRivaCode

jscodeshift
const oldNameFactory = () => {/* */};
const newNameFactory = () => {/* */};

input

output

MicheleRivaCode

Desired output

input

output

import MyHeader from 'components/MyHeader';

export function MyApp(props) {
  return (
    <div>
      <MyHeader {...props.headerProps} />
      <p> Hello, {props.name}!</p> 
    </div>
  )
}
export function MyApp(props) {
  return (
    <div>
      <p> Hello, {props.name}!</p> 
    </div>
  )
}

MicheleRivaCode

export const parser = 'babel'

export default function transformer(file, api) {
  const j = api.jscodeshift;

  const withoutElement = j(file.source)
    .find(j.JSXElement)
    .forEach(function (path) {
      if (path.value.openingElement.name.name === "MyHeader") {
        path.prune();
      }
    })
    .toSource();

  const withoutImport = j(withoutElement)
    .find(j.ImportDefaultSpecifier)
    .forEach(function (path) {
      if (path.value.local.name === "MyHeader") {
        path.parentPath.parentPath.prune();
      }
    })
    .toSource();

  return withoutImport;
};

MicheleRivaCode

const user = {
  data: {
    name: {
      first: "Michele",
      last: "Riva"
    }
  }
}

const middleName = user.data
  && user.data.name
  && user.data.name.middle
  || "No middle name";

console.log(middleName);

Node 12.x

No middle name

MicheleRivaCode

const user = {
  data: {
    name: {
      first: "Michele",
      last: "Riva"
    }
  }
}

const middleName = user.data?.name?.middle ?? "No middle name";

console.log(middleName);

Node 12.x

SyntaxError: Unexpected token '.'

MicheleRivaCode

const user = {
  data: {
    name: {
      first: "Michele",
      last: "Riva"
    }
  }
}

const middleName = user.data?.name?.middle ?? "No middle name";

console.log(middleName);

Node >14.x

"No middle name"

MicheleRivaCode

import get from 'lodash/get';

const user = {
  data: {
    name: {
      first: "Michele",
      last: "Riva"
    }
  }
}

const middleName = get(user, "data.name.middle", "No middle name");

console.log(middleName);
"No middle name"

MicheleRivaCode

import reverse from 'lodash/reverse';
import isString from 'lodash/isString';
import isFunction from 'lodash/isFunction';
import isNull from 'lodash/isNull';
import split from 'lodash/split';
import filter from 'lodash/filter';
import map from 'lodash/map';
import keys from 'lodash/keys';

// and so on...

MicheleRivaCode

import reverse from 'lodash/reverse';
// Array.reverse()

import isString from 'lodash/isString';
// typeof

import isFunction from 'lodash/isFunction';
// typeof

import isNull from 'lodash/isNull';
// typeof

import split from 'lodash/split';
// Array.split()

import filter from 'lodash/filter';
// Array.filter

import map from 'lodash/map';
// Array.map

import keys from 'lodash/keys';
// Object.keys

// and so on...

MicheleRivaCode

What if we have thousands of files to refactor?

MicheleRivaCode

MicheleRivaCode

MicheleRivaCode

in depths

bundling

WebPack

Parcel

Rollup

MicheleRivaCode

Hard

Easier

Easiest

WebPack

Parcel

Rollup

MicheleRivaCode

Hard

Easier

Easiest

Slowest

Slower

Fast

WebPack

Parcel

Rollup

MicheleRivaCode

king of configurations

MicheleRivaCode

king of configurations

Is it still worth it?

MicheleRivaCode

ESBuild

SWC

Vite

Snowpack

is there any better alternative?

(rest in pepperoni)

MicheleRivaCode

esbuild src/myEntry.js --bundle --sourcemap --minify --outfile=dist/mybundle.js

ESBuild

MicheleRivaCode

MicheleRivaCode

ES2019

ES2020

{
  "jsc": {
    "parser": {
      "syntax": "ecmascript",
      "jsx": false,
      "dynamicImport": false,
      "privateMethod": false,
      "functionBind": false,
      "exportDefaultFrom": false,
      "exportNamespaceFrom": false,
      "decorators": false,
      "decoratorsBeforeExport": false,
      "topLevelAwait": false,
      "importMeta": false
    },
    "transform": null,
    "target": "es5",
    "loose": false,
    "externalHelpers": false,
    // Requires v1.2.50 or upper and requires target to be es2016 or upper.
    "keepClassNames": false
  }
}

MicheleRivaCode

SWC can run in a browser thanks to WASM

MicheleRivaCode

Vite

MicheleRivaCode

Vite

MicheleRivaCode

Vite

MicheleRivaCode

// ESM
export default function greet() {
  return "Hello Serbia!";
}

// ESM
import foo, { bar } from "foobar";
// CJS
module.exports = function greet() {
  return "Hello Serbia!";
}

// CJS
const foo, { bar } = require("foobar");

MicheleRivaCode

Vite

MicheleRivaCode

MicheleRivaCode

rest in pepperoni

Snowpack

MicheleRivaCode

Snowpack

rest in pepperoni

MicheleRivaCode

Snowpack

rest in pepperoni

MicheleRivaCode

MicheleRivaCode

/*
 * Skypack CDN - canvas-confetti@1.4.0
 *
 * Learn more:
 *   📙 Package Documentation: https://www.skypack.dev/view/canvas-confetti
 *   📘 Skypack Documentation: https://www.skypack.dev/docs
 *
 * Pinned URL: (Optimized for Production)
 *   ▶️ Normal: https://cdn.skypack.dev/pin/canvas-confetti@v1.4.0-POmgSMO0U5q84otJfYlN/mode=imports/optimized/canvas-confetti.js
 *   ⏩ Minified: https://cdn.skypack.dev/pin/canvas-confetti@v1.4.0-POmgSMO0U5q84otJfYlN/mode=imports,min/optimized/canvas-confetti.js
 *
 */

// Browser-Optimized Imports (Don't directly import the URLs below in your application!)
export * from '/-/canvas-confetti@v1.4.0-POmgSMO0U5q84otJfYlN/dist=es2020,mode=imports/optimized/canvas-confetti.js';
export {default} from '/-/canvas-confetti@v1.4.0-POmgSMO0U5q84otJfYlN/dist=es2020,mode=imports/optimized/canvas-confetti.js';

MicheleRivaCode

MicheleRivaCode

The future is no-bundle

MicheleRivaCode

The future is no-bundle*

*maybe

MicheleRivaCode

The future is bright

MicheleRivaCode

https://kdy1.dev/posts/2022/1/tsc-go

MicheleRivaCode

MicheleRivaCode

MicheleRivaCode

@MicheleRiva

@MicheleRivaCode

/in/MicheleRiva95

www.micheleriva.dev

Copy of Compiling and bundling JS, the painless way - 1hr

By Michele Riva

Copy of Compiling and bundling JS, the painless way - 1hr

  • 390