PBS 128 of X: JavaScript Module Basics
The first milestone in phase 2 of this series is a port of the existing Crypt::HSXKPasswd Perl module to a JavaScript module. Since our first end-goal is a JavaScript module, it’s high time we learned what JavaScript modules are!
In this series we’ve generally taken the approach of only learning things the current ‘right way’™️, but when needed, we’ve started with a summary of the history to give us the broader context needed to make sense of why things are the way they are. For example, without understanding that HTML 5 is the intended replacement for both HTML 4 and XHTML 1 it doesn’t make sense that both <hr>
and <hr />
are valid syntax, or even more bizarrely, that <option selected="selected">
, <option selected>
, and <option selected=true>
are equivalent. If HTML 5 were being designed in a vacuum it simply wouldn’t have these kinds of quirks!
On the one hand, the reason we’ve not covered JavaScript modules up until this point is because of the very messy history of modules in JavaScript. On the other hand, we now have a standard — sure, it’s got some quirks because of that messy history, but at least it’s a single, official, standard! We can now learn one approach, and use it everywhere!
Matching Podcast Episode
Listen along to this instalment on episode 703 of the Chit Chat Across the Pond Podcast.
You can also Download the MP3
Instalment Resources
- The instalment ZIP file — pbs128.zip.
The Problem to be Solved
We want to be able to reuse code in such a way that the internal workings of the code are entirely isolated from our code, without the possibility of any kind of spooky action at a distance.
If I put something into the global scope in my code, it shouldn’t be possible for some third-party code I load to mess with it behind my back. Similarly, it shouldn’t be possible for two pieces of third-party code to effectively fight over the use of a single global variable and break each other’s functionality.
Before modules, if I named a global variable something sensible but generic like currentBalance
, then I had trust that no third-party code I loaded used that same globally scoped variable. One side effect of this mess is a kind of variable-name-land-war among the common libraries, with jQuery being effectively hogging the variable name $
, and Underscore and its competitor loDash the variable name _
.
Without modules, you have to choose — do you want to use Underscore, or do you want to use loDash? Why? Because there’s only one global variable named _
, so since both libraries write their code into that global variable, they collide — the second one loaded simply overwrites the first!
A Potted History of Javascript Modules
A Very Primitive First Attempt
If the biggest problem is littering the global namespace, then a good first step to a solution is to cut down on the use of the global scope. One way to do that is to utilise the fact that functions define their own scopes, and to use self-executing anonymous functions to wrap code in separate scopes, and only store the values returned by these anonymous functions in the global scope. These self-executing un-named functions are more properly known as IIFE, or Immediately Invoked Function Expressions. We covered them way way back in instalment 24.
This is clearly hackery. It makes you code look messy, and it only addresses part of the problem — sure, it keeps the namespace cleaner, but spooky action at a distance is just as possible, and it doesn’t address two libraries publishing their code into the same global variable. IIFEs do not solve the Underscore/loDash problem.
Two ‘Winners’ and a Failed Unification
I like to say that the best thing about being a programmer is that you can scratch you own itch. Well, all JavaScript programmers are programmers, and the lack of any kind of official packaging system for JavaScript made a lot of developers very very itchy indeed!
IIFEs alone simply wouldn’t cut it. They needed to be wrapped in additional functionality to provide a mechanism for renaming variables on import, and, for allowing different code files or third party libraries to specify their interdependencies.
There were lots of early attempts at solving this problem, and it could easily have been the case that one single de facto standard had emerged, but that’s not what happened. The JavaScript community effectively broke into two camps — server-side coders, and client-side coders — and each picked their own winner. Rather than one de facto standard, we got two!
On the browser side the open source RequireJS won out with it’s AMD (Async Module Definition) format, and on the server side the CommonJS project won out when their specification was adopted by NodeJS.
For code that only made sense on one side or the other, there was no problem. A library like PopperJS for making fancy browser tooltips only works in a browser, so it only needs to support AMD, and similarly, a library like fs-extra for better file system interactions only makes sense on the server-side, so it only needs to support CommonJS.
But what about libraries that are just as useful on the server and the browser, things like MomentJS for example? Well, they could bundle two variants of their code, an AMD variant and a CommonJS variant, but that’s a pain in the you-know-what!
That’s where UMD comes in, the very self-aggrandising Universal Module Definition. UMD took the best bits of AMD and CommonJS and provided a format that would work both on the server and in the browser. In fact, UMD is a bit like the wave-particle duality in physics. If you design an experiment that treats an electron as a particle it will behave like a particle, but if you design your experiment to treat those same electrons as waves, they’ll behave as waves. Well, if you treat a UMD module as if it were a CommonJS module it’ll work just fine, and if you treat it like an AMD module it also works just fine!
Rather than being a single standard, UMD is a kind of code-chameleon, adapting to its environment as needed.
Needless to say, this mess of competing standards put more and more pressure on the committee behind the official JavaScript specification to come up with one single official solution to the packaging problem.
(Proverbial) White Smoke — an Official Standard at Last!
Note: ‘white smoke’ is a common idiom in Catholic communities, it means a group has come to an agreement — www.urbandictionary.com/…
We’ve finally arrived at the point where there not only is an official standard (it was released as part of ECMA 6 in 2015), but it is now widely supported on both the client and server side. All the major browsers support ES6 AKA ES Harmony modules, as does the latest LTS version of NodeJS. The reason I made a big point about it being important not to be on an old version of NodeJS in the previous instalment is that older versions of NodeJS only support CommonJS modules, not ES 6 modules.
ES 6 Modules — The Big Picture
Designing ES 6 modules was no small feat — because official modules were late to the party, they had to be as good as the best parts of all the existing systems, and, they had to avoid all of the pain points that were making developers cranky. The design and implementation also had to be flexible enough to work in any JavaScript engine anywhere — not just NodeJS and all the Browsers, but any other JavaScript engine in any other system today, tomorrow, or a decade from now!
The end result of all the hard work is a module system that’s conceptually elegant and simple, but has a lot of complexity under the hood. Most of the time we can ignore that complexity, but it does have some very tangible effects that we need to be aware of.
A Simple Philosophy
Modules are independent scopes that publish a list of variables, functions, or classes for other modules to use, and can consume published variables, functions, and classes from other modules.
Modules are referred to by module specifiers (relative or absolute URIs), and they have to clearly declare what they export, and what they want to import from what other modules. The exporting Module gives the things it exports a name of its choosing, and the importing module can reference that export by any name it likes.
Modules don’t use the global namespace at all!
This approach means that both Underscore and loDash can publish their code as a single export they refer to as _
, but any module using either or both libraries can import that export using any local name they like:
import { _ as uScore } from "./underscore.js";
import { _ as lowD } from "./loDash.js";
When you’re writing modules you import your dependencies and export your functionality, and when you’re just using other people’s modules you simply import the libraries you want to use and give them whatever name you want.
A Complex Implementation
I don’t want to do a deep-dive into the minutiae of how ES 6 modules work under the hood, but it is important that we understand what’s going on at a high level. If you want more detail I highly recommend this excellent blog post from Mozilla’s website: ES modules: A cartoon deep-dive — hacks.mozilla.org/….
It’s very important to understand that there is no single right way to design a code packaging system in general. Each specific design involves many choices, and many of those choices are tradeoffs. The designers of ES 6 modules made some fundamentally different choices to the designers of CommonJS, so the two module systems behave very differently in certain circumstances. If you’ve got years of experience using NodeJS with CommonJS (require()
rather than import
), you really need to pay attention or you’re going to run into problems where your invalid expectations results in bugs that appear inexplicable! (They’re not bugs and they’re totally explicable, but if your brain has the wrong mental model you won’t see it that way!)
With all that said, let’s get stuck in!
To make them as flexible as possible, ES 6 modules are loaded in a three-step process:
- Construction — find all the Modules and load their code.
- Instantiation — create placeholders for all the imports & exports.
- Evaluation — execute the modules to fill in the placeholders.
Step 1 — Construction
Whatever context you’re executing your JavaScript code in, there will be an entry point. It could be a <script>
tag on a web page, the main file specified in your package.json
in a NodeJS project, or something else in some other context.
The JavaScript engine reads the main file and parses it. That is to say, it processes it to convert it from text into a computer-readable representation of JavaScript code. This lets the JavaScript engine scan the main file for import
statements. The engine then sets about finding, reading, and parsing those files, and the process repeats over and over again until all the files are found, read, and parsed.
As the JavaScript engine makes its way through all the files, it builds up a so-called module map — a data structure representing each module as an object. Initially the object simply maps the module’s specifier to a special value that indicates the module is in the process of being loaded, but once it’s read and parsed, the relevant data is stored into the object.
One of the big advantages of this approach is that each module only gets read and parsed once, and only gets represented in the module map by a single object.
This module map is the output form this first step.
Note that no JavaScript code has been executed yet!
It’s important to point out an annoying caveat here — each module is imported based on a module specifier, and the official specification does not define the rules for modules specifiers. Those are left up to the designers of JavaScript engines. There are good reasons for this, but it is annoying, and there are moves afoot to standardise the format as much as possible so as to make code as portable as possible. For now, only a subset of the possible formats will work both in NodeJS and in browsers, so it’s possible to write specifiers that work in NodeJS, but not in browsers, and vice versa.
Step 2 — Instantiation
The module map can be used to build another data structure — a dependency tree. Picture the main module as the root of the tree, and draw a line from that module to every module it directly imports, then go into each imported module and draw a line for every module it imports, and so on until you have a tree-like structure spidering out from the main module to the modules that don’t depend on anything else, the so-called leaves of the tree.
The JavaScript engine uses the dependency tree and the module map to create one single placeholder in memory for each export, and connects each import of that export to the same single placeholder in memory. This means that all modules that import a given export reference a single value, they don’t get separate copies.
The output of this step is all those placeholders in memory with all their connections.
Again, note that no JavaScript code has been executed yet!
Step 3 — Evaluation
Now, finally, each module gets executed exactly once, and in the process, all those placeholders in memory get filled with their appropriate values.
The order of execution is very important. If we started at the root of the dependency tree by executing the main module first, then each and every single placeholder it needed would be empty when it tried to read it, so it would utterly fail. Needless to say that’s not how ES 6 modules get executed! Instead, they get executed in depth-first post-order. That is to say, the leaves of the dependency tree get executed first, then the execution ripples up the tree with the very main module being the very last one to execute. This ensures that each modules’s dependencies are populated when it runs.
How ES 6 Contrasts with CommonJS
The most consequential difference between ES 6 and Common JS is how modules get loaded. CommonJS does not have a three-step process. It simply starts executing code in the main module, and it resolves imports as it meets them, loading them as needed.
This gives a very different execution order, and it has the advantage of allowing the code to generate the identifiers for the modules to load later in the process. This is so-called dynamic loading, and it’s not possible in the original ES 6 module specification (it’s coming, but not yet fully supported).
CommonJS’s approach is easier to understand, and it allows dynamic loading, so what tradeoff did they have to make to allow that? CommonJS can’t deal with loops in the dependency tree. If module A imports module B, and module B module C, then module C module A or B, then you have a loop, and CommonJS fails.
With it’s three-step process and depth-first post-order execution approach, ES 6 Modules can handle dependency loops just fine.
ES6 Modules — The Key Points
- Each module is identified by a module specifier — the format can differ between JavaScript engines
- Each module declares what it imports and what it exports
- Each module is executed exactly once
- There is one single copy of each export, and all imports reference that single copy
- Dependency loops are allowed
- Dynamic loading isn’t supported in the original spec, but support is on the way
Writing an ES 6 Module
The first important subtlety is that ES 6 Modules are always interpreted using the more modern strict JavaScript syntax. For backwards compatibility browsers are more forgiving when they execute regular JavaScript, but not when they execute modules.
Because strict mode is the future, that’s the only JavaScript we’ve learned in this series, so we’re not likely to have picked up the kinds of bad habits that tend to bite JavaScript veterans when they first try to write ES 6 modules.
The second important subtlety is that ES 6 Modules can’t access the global scope, so you must use const
or let
to declare all your variables.
Finally, import
and export
statements must appear at the top-level of a module, that is to say, they can’t be nested inside functions or classes.
With all that out of the way, the important piece of syntax is the export
keyword which is used to export variables, functions, and/or classes from your module.
The problem to be solved by the export
statement is to map a variable, function, or class name that exists within your module’s scope to a public name that can be used to import it elsewhere. You can choose to use the same name internally that you’ll publish externally, but you don’t have to.
For convenience, the export
statement supports a number of syntax shortcuts, and you can have as many export
statements in your module as you like.
Let’s start with the full syntax and work down to some of the shortcuts:
# A single export
export { someLocalName as somePublicName }
# Multiple exports (you can have as many as you want)
export { localName1 as publicName1, localName2 as publicName2 }
# A single export with the same name locally and publicly
export { something }
# A multiple export with the same names locally and publicly
export { something, somethingElse, anotherThing }
# create and export a variable in one step
export let someVariable = "pancakes are yum!";
# create and export a function in one step
export function someFunction(){
console.log('waffles are yum too!');
};
# create and export a class in one step
export class SomeClass{
constructor(){
this._niceFood = "pancakes";
}
get niceFood(){
return this._niceFood;
}
}
Because of how older module systems work, most modules only export one value, so to facilitate interoperability between different types of modules (NodeJS can mix and match CommonJS & ES 6 Modules), and for convenience, each module can mark one single exported item as its default export. The keyword default
is used, and where it gets placed depends on the syntax variant used:
# single export as default
export { someLocalThing as default };
# multiple exports with a default
export { localThing1 as default, localThing2 as somePublicThing };
# a one-step default exports
export default let someVar = "I like cake";
export default function someLocalFunction(){
console.log("I really do like cake!");
}
export default class someLocalClass{
constructor(){
console.log("this constructor does nothing!");
}
}
Importing a Module
Unsurprisingly, the keyword for importing a module is import
. Like with export
there are a few variations on the syntax.
First and foremost, there are two distinctly different modes — one for importing named exports, and one for importing default exports.
Importing Named Exports
The fullest, longest syntax is that for importing named exports, so let’s start there:
# importing a single named export
import { theirNameForIt as myNameForIt } from "./theModule.mjs";
# importing multiple named exports
import { theirName1 as myName1, theirName2 as myName2 } from "./theModule.mjs";
More often than not we want to use the export name as the local name, so this full syntax results in dumb repetition like:
import { something as something } from "./theModule.mjs";
To re-use the export name as the local name we can simply omit the as
part:
import { something } from "./theModule.mjs";
We can mix-and-match when importing multiple things from the same module:
import { thing1, theirThing2 as myThing2 } from "./theModule.mjs";
Importing Default Exports
Now let’s look at the syntax for importing default exports. Because default exports have no name, we always have to give them a name of our choosing. You specify that you want to import the default export by simply omitting the curly braces:
# importing a default export
import myName from "./theModule.mjs";
Note that you can use the named exports syntax to import the default export because the default export has the name default
. The following is equivalent to the example above:
# importing a default export as a named export
import { default as myName } from "./theModule.mjs";
There is another special kind of default-like import — a way to import all the exports without knowing their names:
import * as myName from "./theModule.mjs";
You must specify a name of your choosing for this kind of import, myName
in the example above. What happens with this kind of import is that the specified name becomes a plain object (dictionary) with all the named exports as keys (the default export will be saved with the key 'default'
, so would be at myName.default
in the example above).
Pay Attention to the Curly Braces!
Because the import
statement has these two distinct modes, it’s really important to understand the difference when reading and writing code. It all comes down to the curly braces. If there are no curly braces — curly braces means named exports, no curly braces means default exports (or a global import)!
A Note on Module Specifiers (and why we Need Bundlers)
Both NodeJS and Browsers support relative paths as module specifiers. So specifiers like './someFile.js'
or '../someOtherFolder/someFile.mjs'
will work in both. Relative paths must start with ./
or ../
.
Both NodeJS and Browsers support absolute URLs that don’t contain address schemes. These paths start with a /
.
If you want to write portable code, you’ll need to avoid specifying URL schemes, because NodeJS supports file://
URLs, but browsers don’t, and browsers support http://
and https://
URLs, but NodeJS doesn’t!
NodeJS also supports so-called bare specifiers, these are package names that don’t start with a URL scheme, /
, ./
, or ../
. Node has a complex algorithm for figuring out where to find modules by name, but in general, a bare name will correspond to the name used when installing a module with the npm install
command. We’ve seen this behaviour in action in the previous instalment when we installed MomentJS into our Node project with the command npm install moment
, and then imported it into our little demo app with the line import moment from "moment"
.
Finally, for added confusion, the simplest way to instruct NodeJS to treat a file as an ES 6 module is to name it with the .mjs
file extension. That’s best practice for NodeJS code, but browsers will only load modules from URLs that the return a valid JavaScript MIME type like text/javascript
or application/javascript
. But many web servers don’t do that by default for files with the .mjs
extension, including the latest version of MAMP, so you may well need to reconfigure your web server to support .mjs
files.
For now, there is no easy answer, but thankfully, we don’t need there to be. Bundlers like Rollup and Webpack were invented to deal with this problem at the point in time when a module is exported for publishing, this means we’re free to use Node-compatible specifiers in our code, and our bundler will re-write them for us each time we publish a new release of our module.
Worked Examples
In the instalment’s ZIP file you’ll find three folders, pbs128a
, pbs128b
, and pbs128c
. Each is a NodeJS project.
Because we’re using NodeJS to access modules installed by NPM, we’ll be using bare module specifiers to load our 3rd-party dependencies (like MomentJS), but we’ll be using relative paths as the specifiers for the modules we write ourselves.
Preamble — Initialising Someone Else’s NodeJS Project (npm ci
)
In each of the three example NodeJS project folders you’ll find the code files, a package.json
file, and a package-lock.json
file. What you won’t find is a node_modules
folder. This means none of the dependencies have been installed, and you’ll need to do that before the examples will run.
If you try run one of the examples before installing the dependencies, you’ll get an error saying a needed module couldn’t be found:
bart-imac2018:pbs128 bart% cd pbs128a
bart-imac2018:pbs128a bart% ./main.mjs
internal/process/esm_loader.js:74
internalBinding('errors').triggerUncaughtException(
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'moment' imported from /Users/bart/Documents/Temp/From GitHub/programming-by-stealth/instalmentResources/pbs128/pbs128a/s2xmas.mjs
at new NodeError (internal/errors.js:322:7)
at packageResolve (internal/modules/esm/resolve.js:687:9)
at moduleResolve (internal/modules/esm/resolve.js:728:18)
at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:842:11)
at Loader.resolve (internal/modules/esm/loader.js:89:40)
at Loader.getModuleJob (internal/modules/esm/loader.js:242:28)
at ModuleWrap.<anonymous> (internal/modules/esm/module_job.js:76:40)
at link (internal/modules/esm/module_job.js:75:36) {
code: 'ERR_MODULE_NOT_FOUND'
}
bart-imac2018:pbs128a bart%
You could install the dependencies with the command npm install
(with no additional arguments). This will read the contents of package.json
, contact the NPM servers, and download the latest version of every required package that meets the specifications in package.json
.
If a dependency is specified as, say, ^2.2.0
, that means any version with a higher minor or patch version (NPM uses SemVer) than 2.2.0
is allowed. Imagine that when NPM checks, there is now a version 2.3.0
of our hypothetical package available, what NPM do? It will download version 2.3.0
and update package.json
to specify ^2.3.0
. NPM will also update package-lock.json
with the details of what it actually installed.
If you’re working alone on a project where you don’t care if dependencies get upgraded this behaviour is no problem, but if you’re collaborating with others or trying to nail down a bug, this kind of stealth upgrading is problematic. Unless you expressly want the latest versions of all the dependencies, don’t initialise an NPM project from elsewhere with npm install
, use npm ci
(for clean install) instead.
npm ci
doesn’t determine the dependencies to install from package.json
, it reads the exact versions the project’s author was using from package-lock.json
, and replicates the author’s state in your copy. When using npm ci
, NPM won’t change package.json
or package-lock.json
.
When we get further into the HSXKPasswd project I will expect anyone cloning the repo and submitting pull requests to use npm ci
rather than npm install
, and I’ll be rejecting commits that change package.json
or package-lock.json
unless the intended purpose of the commit was to install or upgrade a package.
Example 1 — A Simple Module
As a first example, let’s convert the code from our sleeps to Christmas calculator from the example at the end of the previous instalment to a module. The code will need to be wrapped in a function, and that function will then need to be exported. Since default exports are the most common approach, we’ll export our function as a default export.
To get started, open the pbs128a
folder from the instalment ZIP in the terminal and install the dependencies with npm ci
.
Creating the Module
The code for the module is very simple, you’ll find it in s2xmas.mjs
:
// import the needed modules (just MomentJS)
import moment from "moment";
// define and export our code as a function
// Note: the function is exported as the default
export default function s2xmas(){
const now = moment();
if(now.date() === 25 && now.month() === 11){
// it's Christmas!
console.log("No more sleeps — it's Christmas 😀🎄🎁")
}else{
const xmas = moment(now.startOf('day')).date(25).month(11);
if(now.isAfter(xmas)) xmas.year(now.year() + 1);
const numDays = Math.abs(now.startOf('day').diff(xmas, 'days'));
console.log(`${numDays} sleeps 😴 till Christmas 🎄`);
}
};
Notice that we start the module by importing our dependencies, just MomentJS in this case, and that we used a default-style import (no curly braces):
import moment from "moment";
This means we chose to import the default export into the module’s scope with the name moment
, a name of our choosing.
Next the module defines and exports the function as the default export, and it does it all in one step:
export default function s2xmas(){
// ...
};
This is equivalent to defining the function and then explicitly exporting it as the default:
function s2xmas(){
// ...
};
export { s2xmas as default };
The contents of the function is simply the code from the previous instalment copied-and-pasted in and indented.
Using the Module
The file we’ll actually run, our entry point is main.mjs
, and it imports and then uses our function:
#!/usr/bin/env node
// import the default export with a name of our choosing
import showSleeps from './s2xmas.mjs';
// call the imported function
showSleeps();
Because our module exported our function as the default export, and not as a named export, we must import it as a default export, i.e., no curly braces. We can name the function anything we like, but I chose simply showSleeps
.
I’m not sure ZIP files preserve Unix file permissions reliably, so you can make sure the main file is executable with chmod +x main.mjs
. Now you can run the main file:
bart-imac2018:pbs128a bart% ./main.mjs
55 sleeps 😴 till Christmas 🎄
bart-imac2018:pbs128a bart%
Example 2 — Named Exports
In the instalment ZIP you’ll find another folder named pbs128b
, this contains an updated version of the sleeps to Christmas calculator that exports two things, a class for calculating the sleeps until Christmas, and a variable containing the default icons. The body of the code is much the same, but there are some added features:
- The function has been converted to a class, allowing more functionality
- A special case has been added for Christmas Eve (thanks to the PBS community on the Podfeet Slack for the suggestion)
- The icons have been made configurable via instance properties (getters and setters)
- The module now exports two named exports (the class and the set of default icons), and a default export, also the class.
Again, remember to install the dependencies with npm ci
before continuing.
You’ll find the code in S2XmasCalculator.class.mjs
:
// import the needed modules (just MomentJS)
import moment from "moment";
// define and export the default icons as a named export
export const defaultIcons = {
sleep: '😴',
christmas: '🎄'
};
// define and export the class as a named export
export class S2XmasCalculator{
// define a basic constructor
constructor(){
this._sleepIcon = defaultIcons.sleep;
this._christmasIcon = defaultIcons.christmas;
}
// add some simple getters and setters
// the sleep icon
get sleepIcon(){
return this._sleepIcon;
}
set sleepIcon(s){
this._sleepIcon = String(s); // force to a string
}
// the christmas icon
get christmasIcon(){
return this._christmasIcon;
}
set christmasIcon(c){
this._christmasIcon = String(c); // force to a string
}
// define the function for generating the text
calculate(){
const now = moment();
if(now.date() === 25 && now.month() === 11){
// it's Christmas!
console.log("No more sleeps — it's Christmas 😀🎄🎁")
}else if(now.date() === 24 && now.month() === 11){
// it's Christmas Eve!
console.log(`Nearly there, just one more sleep till Christmas ${this.christmasIcon}!!!`);
}else{
const xmas = moment(now.startOf('day')).date(25).month(11);
if(now.isAfter(xmas)) xmas.year(now.year() + 1);
const numDays = Math.abs(now.startOf('day').diff(xmas, 'days'));
console.log(`${numDays} sleeps ${this.sleepIcon} till Christmas ${this.christmasIcon}`);
}
}
};
// also export the class as the default export
export { S2XmasCalculator as default };
Like in the first example, the module starts by importing MomentJS.
Next the set of default icons is defined as a plain object (dictionary), and exported as a named export. Note that because we’re using the shorter all-in-one syntax, the object is exported with the same name it has within the module, i.e. defaultIcons
;
If we wanted to export it with a different name, say icons
, we’d use a two-step process:
const defaultIcons = {
sleep: '😴',
christmas: '🎄'
};
export { defaultIcons as icons };
The class is defined and exported as a named export, again, in one step, and again, it will be exported with the name it was created with, S2XmasCalculator
.
The contents of the class is pretty basic — the constructor sets default values for the two instance properties, there are getters and setters for each property (.sleepIcon
& .christmasIcon
), and there’s one instance function to do the actual calculation (calculate()
).
The contents of the .calculate()
function is mostly the same as that from the s2xmas()
function in the previous example, but with an added else if
to deal with Christmas Eve.
Notice that at the end of the module the class is exported again, as the default export:
export { S2XmasCalculator as default };
This makes it easy to import just the class as a default export, or, to use named exports to import both the class and the icons.
Again, our entry point is for running the calculator is main.mjs
:
#!/usr/bin/env node
// import the class as a named export without re-naming it
import { S2XmasCalculator } from './S2XmasCalculator.class.mjs';
// create an instance of the imported class
const calc = new S2XmasCalculator();
// do the calculation
calc.calculate();
// change the icons
calc.sleepIcon = '💤';
calc.christmasIcon = '🎅';
// do the calculation again
calc.calculate();
We start by importing the class and giving it a name of our choosing, SleepsCalc
in this case. Notice that I chose to use the default export (no curly braces on the import
statement), even though I could have used named exports. (We’ll used named imports in the final example below.)
With the class imported we then create and instance of it named calc
, and call it’s .calculate()
function.
Next, we use the setters to update the icons, then we call the .calculate()
function again.
Again, before running the script the first time, make sure it’s executable with chmod +x main.mjs
, then you can run it:
bart-imac2018:pbs128b bart% ./main.mjs
55 sleeps 😴 till Christmas 🎄
55 sleeps 💤 till Christmas 🎅
bart-imac2018:pbs128b bart%
Example 3 — Importing Multiple Exports with the Same Name
As a final example, and to bring things back to the Underscore & loDash example, I’ve created a second version of our class that uses the Luxon date library rather than MomentJS. Luxon is Moment’s successor, so we’ll need to get used to using it rather than Moment soon enough!
You’ll find the code for this final example in the pbs128c
folder in the instalment ZIP. Again, remember to install the dependencies with npm ci
!
The folder contains two module definitions, the file S2XmasCalculator-Moment.class.mjs
contains the version of our class from the previous instalment, still with it’s named and default exports, and still exporting the class as S2XmasCalculator
. The folder also contains the file S2XmasCalculator-Luxon.class.mjs
, and this file contains a new version of the class that imports and uses Luxon. However, similar to how both Underscore and loDash export themselves as _
, this module also exports the class as S2XmasCalculator
. We now have two modules exporting different code with the same name.
Before we look at how we can use these two modules within a single main file, let’s take a moment to have a very quick look at Luxon.
The first thing to note is that Moment uses a single default export, while Luxon uses only named exports. This means that we import Moment without curly braces:
import moment from "moment";
But we import the one class we want, DateTime
from Luxon with curly braces:
import { DateTime } from "luxon";
Luxon uses a very different philosophy to Moment, so the date manipulation code is completely different. It is however, much more readable in my opinion:
// define the function for generating the text
calculate(){
const today = DateTime.now().startOf('day');
if(today.day === 25 && today.month === 12){
// it's Christmas!
console.log("No more sleeps — it's Christmas 😀🎄🎁")
}else if(today.day === 24 && today.month === 12){
// it's Christmas Eve!
console.log(`Nearly there, just one more sleep till Christmas ${this.christmasIcon}!!!`);
}else{
let xmas = DateTime.fromObject({ day: 25, month: 12, year: today.year });
if(today > xmas) xmas = xmas.plus({ year: 1 });
const numDays = Math.abs(today.diff(xmas, 'days').as('days'));
console.log(`${numDays} sleeps ${this.sleepIcon} till Christmas ${this.christmasIcon}`);
}
}
Notice that the month is 12
not 11
, since Luxon counts January as 1
not 0
like Moment does. Also notice that Luxon uses getters and setters, so it’s not now.month()
, but now.month
;
Another huge advantage is that Luxon DateTime
objects provide a .valueOf()
instance function, making them compatible with JavaScript’s regular comparison operators, hence if(today > xmas)
rather than if(now.isAfter(xmas))
.
Back to our example — our entry point is still main.mjs
:
#!/usr/bin/env node
// import the MomentJS and Luxon versions of the class and default icons using named imports
import { S2XmasCalculator as SleepsCalcMJS,
defaultIcons as iconsMJS } from './S2XmasCalculator-Moment.class.mjs';
import { S2XmasCalculator as SleepsCalcLux,
defaultIcons as iconsLux } from './S2XmasCalculator-Luxon.class.mjs';
// show both sets of default icons
console.log('MomentJS Variant Icons:', iconsMJS);
console.log('Luxon Variant Icons:', iconsLux);
// create an instance of each of the classes
const calcMJS = new SleepsCalcMJS();
const calcLux = new SleepsCalcLux();
// do the calculation with each
calcMJS.calculate();
calcLux.calculate();
Notice the use of the full import syntax to import two named imports from each module, and re-name both on import. This is how ES6 modules allow us to easily work around name collisions.
Again, make sure you’ve initialised node with npm ci
, and that the permissions are correct on the entry point with chmod +x main.mjs
, and then run main.mjs
to see our two imports in action:
bart-imac2018:pbs128c bart% ./main.mjs
MomentJS Variant Icons: { sleep: '😴', christmas: '🎄' }
Luxon Variant Icons: { sleep: '💤', christmas: '🎅' }
55 sleeps 😴 till Christmas 🎄
55 sleeps 💤 till Christmas 🎅
bart-imac2018:pbs128c bart%
Final Thoughts
There was a lot to digest in this instalment, but we’ll be doing so much importing and exporting that it will hopefully become second nature soon enough.
Before we’re ready to start work on our port of Crypt::HSXKPasswd
to a JavaScript module we still have some more learning to do — we’re still missing three pieces of the puzzle:
- A Linter
- A documentation generator
- A test suite
In the next instalment, guest teacher Helma from the Netherlands will step in to introduce us all to ESLint, the JavaScript Linter we’ll be using in this series.
I’ll be using the free time created by Helma’s kind help to finish my research on documentation generators, and to make a final decision on which one we’ll be using. I’ll be back the instalment after next with an introduction to which ever one I choose.