Modules

ES2015 vs. CommonJS

whoami

  • Zirak
  • I like JS
  • http://zirak.me
  •  

  • Did I forget to record my screen?
  • But enough about me

ES2015 includes module definitions

  • Long anticipated
  • But modules have been around in Node for several years (CommonJS)
  • How are ES modules different?
  • And why does Node have difficulty importing CommonJS modules?

Quick overview: CommonJS


                // CommonJS
                // a.js
                module.exports = {
                    foo: 4,
                    bar: 7,
                    baz: 12
                };

                // b.js
                var a = require('./a');
                console.log(a.bar); // 7
            
  • module.exports is a POJSO, can be anything and changed anytime
  • require is just another regular function
  • require result is the exported POJSO
  • At the end of the day, a library

Quick overview: Modules


                // Modules
                // a.js
                export const number = 4;
                export default 7;

                // b.js
                import florg, { number } from './a.js';
                console.log(florg + number); // 11
            
  • export and import are resolved at module parse time
  • export exposes variable bindings
  • import receives variable bindings
  • Have one (optional) default export
  • Note: Syntax is not object destructuring!
  • Since it's a part of the language itself, it can use dirty magic tricks

In one word:

Static (ES2015)

vs.

Dynamic (CommonJS)

When can we import/export?

Parse time (ES) vs. Runtime (CommonJS)

  • import/export must be top-level:
    
                            // valid
                            import foo from 'foo';
                            
                            // invalid
                            if (false) {
                                import bar from 'bar';
                            }
    
                            // invalid
                            setTimeout(function () {
                                export let quz = 14;
                            });
                        
  • require/module.exports can be anywhere:
    
                            // valid
                            if (Math.random() > 0.1) {
                                exports.foobar = 7;
                            } else {
                                require('rimraf')('/');
                            }
                        

TOC/TOU

Where can we use imported variables?

  • imported bindings are determined at parse time and hoisted:
    
                        // valid
                        console.log('this is ok', foo)
                        import foo from 'foo';
                    
  • require is just another function call, so assignment behaves as usual:
    
                        // throws an error
                        console.log('oh no an error', foo); // ReferenceError
                        const foo = require('./foo');
                    

Where can we import from?

String literal (ES) vs. any expression (CommonJS)

  • The RHS of import must be a string literal:
    
                        // valid
                        import foo from 'foo';
    
                        // invalid
                        import foo from 'f' + 'oo';
                        import foo from `template-string`;
                        import foo from 6;
                        import foo from {};
                    
  • Since require is a regular function, we can pass anything we want:
    
                        require(hasTheLargeHadronColliderDestroyedTheWorldYet ?
                            'left-pad' :
                            Math.random() * Date.now() / 2 + 7 + '.js'
                        );
                    

What do we import/export?

Variable binding (ES) vs. POJSO (CommonJS)

  • import receives a variable binding, export exposes one:
    
                        // foo.js
                        export let foo = 4;
                        export function incFoo() { foo += 1; }
                        // main.js
                        import { foo, incFoo } from './foo.js';
                        console.log(foo); // 4
                        incFoo();
                        console.log(foo); // 5
                    
  • With CommonJS it's...complicated.
    
                        // wut.js
                        var foo = 4;
                        module.exports = { foo, bar: 7, incFoo, incBar };
                        function incFoo() { foo += 1; }
                        function incBar() { module.exports.bar += 1; }
                    
    
                        // main.js
                        var wut = require('./wut');
                        console.log(wut.foo, wut.bar); // 4, 7
                        incFoo(); incBar();
                        console.log(wut.foo, wut.bar); // 4, 8
                    
  • Since an object is exported changing the object works as expected.

In Summary

  • CommonJS and ES modules accomplish similar things, but have differing philosophies
  • CommonJS is dynamic, ES is static
  • ES modules expose variable bindings, CommonJS modules expose objects

Addendum: Modules browser support

At the time of writing, 2017/05/10
  • Safari stable (10.1)
  • Edge 15 (behind flag)
  • Firefox 54+ (behind flag)
  • Chrome 60+ (behind flag)

  • In other words, soon-ish

Random module goodies


                // Imported bindings are const (read-only)
                import foo from './foo.js';
                foo = 4; // TypeError: Assignment to constant variable.
            

                // You can forward the exports of another module
                export * from 'foo';
                // Maybe more on that later
            

                // Proposal: dynamic imports https://github.com/tc39/proposal-dynamic-import
                // Implements an `import` function
                import(hasTheLargeHadronColliderDestroyedTheWorldYet ?
                    'left-pad' :
                    Math.random() * Date.now() / 2 + 7 + '.js'
                ).then(module => {
                    // win
                });
            

                // Proposal: Loaders https://github.com/whatwg/loader
                // Allows customizing the import process on several levels
                // (all sorts of urls, transpiling on-the-fly, ...)
                // Nothing concrete yet!
            

Forward-exports are weird


                // What if there are duplicate names?
                export let foo = 4;
                export * from 'exports-foo';
            

                // What if multiple modules declare the same identifier?

                // mid.js
                export * from 'exports-foo';
                export * from 'also-exports-foo';
            
  • The answer is It Depends: How are you importing?

                import { foo } from './mid.js'; // SyntaxError
            

                import * as mid from './mid.js'; // no error
                console.log(mid.foo); // undefined
            

Thank you

Questions?