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
- 530