ES6 export as code brick


What is a software brick in modern ES2015+ applications? Variable, function, class, module? My answer is export. Currently, we place our JavaScript code into ES6-modules (files) and bind modules with each other using the import statement:

import {export1, export2} from "module-name";

So, every export is a minimal code object which can be used by developers in complex programs (applications).


Script Files

Generally, all code is placed into separate files — script files in JavaScript. This post focuses on the modern ES2015+ (ES6) approach, so these script files are called ES6-modules. Unlike Java, an ES6-module can contain more than one class (function, object, etc.) in a single file. The JS engine executes every ES6-module on import and saves the results in the module’s scope. Other ES6-modules can access these results if they are exported by the module:

// module 'cube.mjs'
export default function cube(x) {
    return x * x * x;
}

But first, we need to know the path to the script file in our application. Only then can we address any export from outside code:

// module 'main.mjs'
import cube from './cube.mjs';

const y = cube(2); // 8

Paths to script files can be absolute or relative:

// absolute
import cube from '/home/alex/cube.mjs'; // for server-side
import cube from 'https://domain.com/cube.mjs'; // for browser
// relative
import cube from './cube.mjs'; // both for server & browser

For server-side (Node.js), you can also use package-style imports:

import cube from '@vendor/package/path/to/cube.mjs';

However, relative paths must always begin with ./:

import cube from 'cube.mjs'; // failure

Error example from Chrome browser:

Relative references must start with either "/", "./", or "../".

The First import Execution

All code inside an ES6-module runs just once — on the first import of the module. The resulting exports are then used to bind this module’s code with other modules.


Module Scope

An ES6-module can export a hello function that prints a hello message and saves the addressee (to) as the new addresser (from):

// ./sub.mjs
console.log(`es6-module is imported.`);
let from = 'module';

export function hello(to) {
    console.log(`Hello to ${to} from ${from}.`);
    from = to;
}

Even if imported multiple times, the string "es6-module is imported." will be printed just once:

import {hello} from './sub.mjs';

hello('main');
hello('again');

Output:

es6-module is imported.
Hello to main from module.
Hello to again from main.

Asynchronous Import

An ES6-module can also perform asynchronous operations and compose its exports:

console.log(`es6-module is imported.`);
const init = new Promise((resolve) => {
    setTimeout(() => {
        function hello(to) {
            console.log(`Hello to ${to} from ${from}.`);
            from = to;
        }

        resolve(hello);
    }, 3000);
});
let from = 'module';

/** @type {function(to: string)} */
const hello = (await init);

console.log(`'hello' function is created.`);
export {hello}

Code execution will pause on the first import and wait for the asynchronous result:

import {hello} from './async.mjs';

hello('main');
hello('again');

Output:

es6-module is imported.
'hello' function is created. // after 3 seconds
Hello to main from module.
Hello to again from main.

Zero Export

It’s possible for an ES6-module to perform some useful work without exporting anything:

function cleanUpFiles() {}

setInterval(() => {
    cleanUpFiles();
}, 60 * 1000);

In this case, you can still import the module:

import './cleaner.mjs';

Even if imported multiple times, the cleanup function will only start once — on the first import.


Summary

Every reusable component (brick) we use to build an application (wall) is an export in some ES6-module:

LOGO

I imagine all available code fragments (classes, functions, objects) as exports of ES6-modules and can address every fragment using the notation Path_To_File.exportName. I can do it in documentation for my apps and, more importantly, I can do it in code of my apps. Typical class constructor in my own code:

class Demo {
    constructor(spec) {
        /** @type {TeqFw_Core_Shared_Util_Cast.castInt|function} */
        const castInt = spec['TeqFw_Core_Shared_Util_Cast.castInt'];

        /** @type {TeqFw_Core_Shared_Util_Cast.castStr|function} */
        const castStr = spec['TeqFw_Core_Shared_Util_Cast.castStr'];
    }
}

This pure JavaScript example demonstrates my Dependency Injection container @teqfw/di, which helps compose complex applications (walls) from individual exports (bricks).


I believe that JavaScript needs namespaces (as seen in other serious languages) to better organize exports. I hope to write more about this in future posts.