Building your own Module Bundler

Tan Li Hau

One level deeper

Contents

 

- What is a module bundler

- Live Coding (write a module bundler)

What is a

module bundler?

Why do we need a

module bundler?

<html>

    ...

    <script src="/src/foo.js" />

    <script src="/src/bar.js" />

    <script src="/src/baz.js" />

    <script src="/src/qux.js" />

    <script src="/src/quux.js" />

    <script src="/src/corge.js" />

    <script src="/src/grault.js" />

    <script src="/src/garply.js" />

    <script src="/src/waldo.js" />

    <script src="/src/fred.js" />

    ...

</html>

<html>

    ...

    <script src="/src/foo.js" />

    <script src="/src/bar.js" />

    <script src="/src/baz.js" />

    <script src="/src/qux.js" />

    <script src="/src/quux.js" />

    <script src="/src/corge.js" />

    <script src="/src/grault.js" />

    <script src="/src/garply.js" />

    <script src="/src/waldo.js" />

    <script src="/src/fred.js" />

    ...

</html>

10 requests for JS code!

<html>

    ...

    <script src="/dist/bundle.js" />

</html>

1 request for JS code!

IDEALLY

<html>

    ...

    <script src="/dist/bundle.js" />

</html>

  • Order of each "file" in the bundle?

  • Naming, scope conflicts between "file"s?

  • Unused "file" within the bundle?

To describe the dependency relationship amongst modules

MODULE SYSTEM

CommonJS

ES Modules

const foo = require('./foo');

module.exports = bar;

import foo from './foo';

export default bar;

How do we bundle?

The "        " way

The "        " way

// circle.js

const PI = 3.141;

export default function area(radius) {

    return PI * radius * radius;

}

 

// square.js

export default function area(side) {

    return side * side;

}

 

// app.js

import squareArea from './square';

import circleArea from './circle';

 

console.log('Area of square: ', squareArea(5))

console.log('Area of circle', circleArea(5))

// webpack-bundle.js

 

const modules = {

    'circle.js': function(exports, require) {

        const PI = 3.141;

        exports.default = function area(radius) {

            return PI * radius * radius;

        }

    },

    'square.js': function(exports, require) {

        export.default = function area(side) {

            return side * side;

        }

    },

    'app.js': function(exports, require) {

        const squareArea = require('square.js').default;

        const circleArea = require('circle.js').default;

        console.log('Area of square: ', squareArea(5))

        console.log('Area of circle', circleArea(5))

    }

}

 

webpackStart({

    modules,

    entry: 'app.js'

});

// ... continue webpack-bundle.js

function webpackStart({ modules, entry }) {

    const moduleCache = {};

 

    const require = (moduleName) => {

        // if in cache, return the cached version

        if (moduleCache[moduleName]) {

            return moduleCache[moduleName];

        }

 

        const exports = {};

        // NOTE: you will thank me later

        moduleCache[moduleName] = exports;

 

        // "require"-ing the module,

        // exported stuff will assigned to "exports"

        modules[moduleName](exports, require);

 

        return moduleCache[moduleName];

    }

 

    // start the program

    require(entry);

}

// rollup-bundle.js

 

const PI = 3.141;

 

function circle$area(radius) {

    return PI * radius * radius;

}

function square$area(side) {

    return side * side;

}

 

console.log('Area of square: ', square$area(5));

console.log('Area of circle', circle$area(5));

// shape.js

const circle = require('./circle');

module.exports.PI = 3.141;


// circle.js

const PI = require('./shape');

const _PI = PI * 1

module.exports = function(radius) {

     return _PI * radius * radius;

}

// rollup-bundle.js

const _PI = PI * 1;

function circle$Area(radius) {

    return PI * radius * radius;

}

 

const PI = 3.141;

SUMMARY

- Larger bundle

- Wrapped each module with 1 function

- Has a "runtime" that glues static import and dynamic import

- Smaller bundle

- Flat bundle

 

- Everything is defined into global scope. Uses browser's module system for dynamic import

LETS WRITE OUR OWN BUNDLER

LETS WRITE OUR OWN BUNDLER

(The        way)

AST

(Abstract Syntax Tree)

RESOLVING

// <root>/a.js

import './b.js'

 

// <root>/foo/a.js

import './b.js'

// <root>/a.js

import './b.js'

 

// <root>/foo/a.js

import './b.js'

request path is the same,

but the requested file is different

// <root>/a.js

import './b.js'

 

// <root>/foo/a.js

import './b.js'

// /foo/bar/baz/a.js

import './b'

// /foo/bar/baz/a.js

import './b'

Load as File

 

/foo/bar/baz/b

/foo/bar/baz/b.js

/foo/bar/baz/b.json

/foo/bar/baz/b.node

// /foo/bar/baz/a.js

import './b'

Load as Directory

 

if /foo/bar/baz/b/package.json :

   find "main" in package.json and load the file

else:

   /foo/bar/baz/b/index.js

   /foo/bar/baz/b/index.json

   /foo/bar/baz/b/index.node

// /foo/bar/baz/a.js

import 'b'

// /foo/bar/baz/a.js

import 'b'

Load as node_module

 

/foo/bar/baz/node_modules/b

/foo/bar/node_modules/b

/foo/node_modules/b

/node_modules/b

  • extensions
    • 'index.js', 'index.json', 'index.ts', ...
  • alias
    • 'app/index.js' instead of '../../../index.js'
  • modules
    • 'node_modules', ​'bower_components'
  • mainFields​​
    • 'main', 'browser' in package.json
  • ​symlinks
  • ...

 

https://webpack.js.org/configuration/resolve/

https://github.com/webpack/enhanced-resolve

// webpack-bundle.js

 

const modules = {

    'circle.js': function(exports, require) {

        const PI = 3.141;

        exports.default = function area(radius) {

            return PI * radius * radius;

        }

    },

    'square.js': function(exports, require) {

        export.default = function area(side) {

            return side * side;

        }

    },

    'app.js': function(exports, require) {

        const squareArea = require('square.js').default;

        const circleArea = require('circle.js').default;

        console.log('Area of square: ', squareArea(5))

        console.log('Area of circle', circleArea(5))

    }

}

 

webpackStart({

    modules,

    entry: 'app.js'

});

// ... continue webpack-bundle.js

function webpackStart({ modules, entry }) {

    const moduleCache = {};

 

    const require = (moduleName) => {

        // if in cache, return the cached version

        if (moduleCache[moduleName]) {

            return moduleCache[moduleName];

        }

 

        const exports = {};

        // NOTE: you will thank me later

        moduleCache[moduleName] = exports;

 

        // "require"-ing the module,

        // exported stuff will assigned to "exports"

        modules[moduleName](exports, require);

 

        return moduleCache[moduleName];

    }

 

    // start the program

    require(entry);

}

Summary

Is this all?

Is this all?

- import CSS, SVG, PNG, TS, ...

- commonjs, amd, dynamic import...

- optimisation: minification, code splitting, ...

- webpack dev server, watching, hot reloading

- ...

Is this all?

- import CSS, SVG, PNG, TS, ...

- commonjs, amd, dynamic import...

- optimisation: minification, code splitting, ...

- webpack dev server, watching, hot reloading

- ...

Is this all?

- import CSS, SVG, PNG, TS, ...

- commonjs, amd, dynamic import...

- optimisation: minification, code splitting, ...

- webpack dev server, watching, hot reloading

- ...

THANK YOU

WHY NOT <SCRIPT TYPE="MODULE" /> ?

<html>

    ...

    <script type="module" src="/src/bar.mjs" />

</html>

<html>

    ...

    <script type="module" src="/src/bar.mjs" />

</html>

// bar.mjs

import foo from './foo.mjs';

 

export default bar;

Building your own Module Bundler

By Li Hau Tan

Building your own Module Bundler

  • 766