Working with ES6 modules Flashcards
What are named exports?
If we put export
in front of a named entity inside a module, it becomes a named export of that module. All other entities are private to the module.
/* module1.mjs */ // Named exports export const name = 'John', surname = 'Doe'; export function getName() { return `${name} ${surname}`; }
“Named exports, named imports, namespace imports” (exploringjs.com). Retrieved July 30, 2024.
What are named imports?
Named imports are imports
done withing curly brakets import named exports?
/* module1.mjs */ // Named exports export const name = 'John', surname = 'Doe'; export function getName() { return `${name} ${surname}`; } /* module2.mjs */ // Named imports import {name, surname, getName} from './module1.mjs'; assert.equal(name, 'John'); assert.equal(getName(), 'John Doe');
The string after from
is called a module specifier. It identifies from which module we want to import.
“Named exports, named imports, namespace imports” (exploringjs.com). Retrieved July 30, 2024.
What are namespace imports?
namespace imports allow you to fetch all the named imports at once adding import * as [namespace]
/* module1.mjs */ // Named exports export const name = 'John', surname = 'Doe'; export function getName() { return `${name} ${surname}`; } /* module2.mjs */ // Namespace import import * as module from './module1.mjs'; assert.equal(module.name, 'John'); assert.equal(module.getName(), 'John Doe');
The string after from
is called a module specifier. It identifies from which module we want to import.
“Named exports, named imports, namespace imports” (exploringjs.com). Retrieved July 30, 2024.
What is a default export?
A default export is mainly used when a module only contains a single entity (even though it can be combined with named exports).
//===== lib2a.mjs ===== export default function getHello() { return 'hello'; }
A default export is the exception to the rule that function declarations always have names: In the previous example, we can omit the name getHello.
//===== lib2b.mjs ===== export default 123; // (A) instead of `const` There can be at most one default export. That’s why const or let can’t be default-exported (line A). //===== main2.mjs ===== import lib2a from './lib2a.mjs'; assert.equal(lib2a(), 'hello'); import lib2b from './lib2b.mjs'; assert.equal(lib2b, 123);
“Default exports and imports” (exploringjs.com). Retrieved September 10, 2024.
What are dynamic imports?
The import()
syntax, commonly called dynamic import, is a function-like expression that allows loading an ECMAScript module asynchronously and dynamically into a potentially non-module environment.
Unlike static imports, dynamic imports are only evaluated when needed, and permit greater syntactic flexibility.
import * as mod from "/my-module.js"; import("/my-module.js").then((mod2) => { console.log(mod === mod2); // true });
“import() - JavaScript | MDN” (MDN Web Docs). Retrieved September 10, 2024.
What is the syntax of static imports?
import defaultExport from "module-specifier"; import * as name from "module-specifier"; import { export1 } from "module-specifier"; import { export1 as alias1 } from "module-specifier"; import { default as alias } from "module-specifier"; import { export1, export2 } from "module-specifier"; import { export1, export2 as alias2, /* … */ } from "module-specifier"; import { "string name" as alias } from "module-specifier"; import defaultExport, { export1, /* … */ } from "module-specifier"; import defaultExport, * as name from "module-specifier"; import "module-specifier";
“Syntax” (MDN Web Docs). Retrieved September 10, 2024.
What is a module specifier?
The string after from
is called a module specifier. It identifies from which module we want to import.
“Named exports, named imports, namespace imports” (exploringjs.com). Retrieved September 10, 2024.
What are the various types of module specifiers?
There are three types of module specifiers:
Absolute specifiers are full URLs – for example:
'https://www.unpkg.com/browse/yargs@17.3.1/browser.mjs' 'file:///opt/nodejs/config.mjs'
Absolute specifiers are mostly used to access libraries that are directly hosted on the web.
Relative specifiers are relative URLs (starting with '/'
, './'
or '../'
) – for example:
'./sibling-module.js' '../module-in-parent-dir.mjs' '../../dir/other-module.js'
Relative specifiers are mostly used to access other modules within the same code base.
Bare specifiers are paths (without protocol and domain) that start with neither slashes nor dots. They begin with the names of packages (as installed via a package manager such npm). Those names can optionally be followed by subpaths:
'some-package' 'some-package/sync' 'some-package/util/files/path-tools.js'
Bare specifiers can also refer to packages with scoped names:
'@some-scope/scoped-name' '@some-scope/scoped-name/async' '@some-scope/scoped-name/dir/some-module.mjs'
Each bare specifier refers to exactly one module inside a package; if it has no subpath, it refers to the designated “main”
module of its package.
“Kinds of module specifiers” (exploringjs.com). Retrieved September 10, 2024.
What is a default import?
Default exports need to be imported with the corresponding default import syntax. The simplest version directly imports the default:
import myDefault from "/modules/my-module.js";
Since the default export doesn’t explicitly specify a name, you can give the identifier any name you like.
It is also possible to specify a default import with namespace imports or named imports. In such cases, the default import will have to be declared first. For instance:
import myDefault, * as myModule from "/modules/my-module.js"; // myModule.default and myDefault point to the same binding
or
import myDefault, { foo, bar } from "/modules/my-module.js";
Importing a name called default
has the same effect as a default import. It is necessary to alias the name because default is a reserved word.
import { default as myDefault } from "/modules/my-module.js";
“Default import” (MDN Web Docs). Retrieved September 11, 2024.
What is re-exporting?
A module library.mjs
can export one or more exports of another module internal.mjs
as if it had made them itself. That is called re-exporting.
//===== internal.mjs ===== export function internalFunc() {} export const INTERNAL_DEF = 'hello'; export default 123; //===== library.mjs ===== // Named re-export [ES6] export {internalFunc as func, INTERNAL_DEF as DEF} from './internal.mjs'; // Wildcard re-export [ES6] export * from './internal.mjs'; // Namespace re-export [ES2020] export * as ns from './internal.mjs';
The wildcard re-export turns all exports of module internal.mjs
into exports of library.mjs
, except the default
export.
The namespace re-export turns all exports of module internal.mjs
into an object that becomes the named export ns
of library.mjs
. Because internal.mjs
has a default
export, ns
has a property .default
.
“Re-exporting” (exploringjs.com). Retrieved September 11, 2024.
Explain why imports are read-only views on exports
There are two benefits to handling imports this way:
- It is easier to split modules because previously shared variables can become exports.
- This behavior is crucial for supporting transparent cyclic imports.
Consider the following two modules:
counter.mjs
main.mjs
counter.mjs
exports a (mutable!) variable and a function:
export let counter = 3; export function incCounter() { counter++; }
main.mjs
name-imports both exports. When we use incCounter()
, we discover that the connection to counter
is live – we can always access the live state of that variable:
import { counter, incCounter } from './counter.mjs'; // The imported value `counter` is live assert.equal(counter, 3); incCounter(); assert.equal(counter, 4);
Note that while the connection is live and we can read counter, we cannot change this variable (e.g., via counter++
).
“Imports are read-only views on exports” (exploringjs.com). Retrieved September 12, 2024.
List ESM and commonJs module specifier differences
All specifiers, except bare paths, must refer to actual files. That is, ESM does not support the following CommonJS features:
- CommonJS automatically adds missing filename extensions.
- CommonJS can import a directory dir if there is a dir/package.json with a “main” property.
- CommonJS can import a directory dir if there is a module dir/index.js.
“ES module specifiers on Node.js” (exploringjs.com). Retrieved September 13, 2024.
List and explain Node.js supported module extensions
Node.js supports the following default filename extensions:
-
.mjs
for ES modules -
.cjs
for CommonJS modules
The filename extension .js
stands for either ESM or CommonJS. Which one it is is configured via the “closest” package.json
(in the current directory, the parent directory, etc.). Using package.json in this manner is independent of packages.
In that package.json, there is a property "type"
, which has two settings:
-
"commonjs"
(the default): files with the extension .js or without an extension are interpreted as CommonJS modules. -
"module"
: files with the extension .js or without an extension are interpreted as ESM modules.
“Filename extensions on Node.js” (exploringjs.com). Retrieved September 13, 2024.
What does import.meta
hold?
The object import.meta
holds metadata for the current module.
import.meta.url
- contains a string with the URL of the current module’s file – for example:
'https://example.com/code/main.mjs'
On Node.js, import.meta.url
is always a string with a file: URL
– for example:
'file:///Users/carpasse/code/main.mjs'
“import.meta.url
” (exploringjs.com). Retrieved September 16, 2024.
How can you get a URL instance that points to a file data.txt
that sits next to the current module?
When working with import.meta.url, URL
constructor is especially useful:
new URL(input: string, base?: string|URL)
Parameter input
contains the URL to be parsed. It can be relative if the second parameter, base
, is provided.
In other words, this constructor lets us resolve a relative path against a base URL:
> new URL('other.mjs', 'https://example.com/code/main.mjs').href 'https://example.com/code/other.mjs' > new URL('../other.mjs', 'https://example.com/code/main.mjs').href 'https://example.com/other.mjs'
This is how we get a URL instance that points to a file data.txt that sits next to the current module:
const urlOfData = new URL('data.txt', import.meta.url);
“import.meta.url and class URL” (exploringjs.com). Retrieved September 16, 2024.
How can you convert between file: URLs and paths?
The Node.js module url
has two functions for converting between file: URLs and paths:
fileURLToPath(url: URL|string): string // Converts a file: URL to a path. pathToFileURL(path: string): URL // Converts a path to a file: URL.
fileURLToPath
ensures the correct decodings of percent-encoded characters as well as ensuring a cross-platform valid absolute path string. Therefore, instead of:
new URL('file:///tmp/with%20space.txt', import.meta.url).pathname
It is better to use fileURLToPath()
:
import * as url from 'node:url'; url.fileURLToPath('file:///tmp/with%20space.txt')
Similarly, pathToFileURL()
does more than just prepend 'file://'
to an absolute path.
“Converting between file: URLs and paths” (exploringjs.com). Retrieved September 16, 2024.
How can you load a module dynamically?
With import()
operator:
import(moduleSpecifierStr) .then((namespaceObject) => { console.log(namespaceObject.namedExport); });
This operator is used like a function, receives a string with a module specifier and returns a Promise
that resolves to a namespace object. The properties of that object are the exports of the imported module.
“Dynamic imports via the import() operator” (exploringjs.com). Retrieved September 17, 2024.
Why is import()
an operator and not a function?
import()
looks like a function but couldn’t be implemented as a function:
- It needs to know the URL of the current module in order to resolve relative module specifiers.
- If
import()
were a function, we’d have to explicitly pass this information to it (e.g. via an parameter). - In contrast, an operator is a core language construct and has implicit access to more data, including the URL of the current module.
“Dynamic imports via the import() operator” (exploringjs.com). Retrieved September 17, 2024.
What is a top level await
?
A top level await
is when you use the await
keyword on its own (outside of an async function) at the top level of a module. This means that modules with child modules that use await
will wait for the child modules to execute before they themselves run, all while not blocking other child modules from loading.
Here is an example of a simple module using the Fetch
API and specifying await
within the export
statement. Any modules that include this will wait for the fetch to resolve before running any code.
// fetch request const colors = fetch("../data/colors.json").then((response) => response.json()); export default await colors;
“Top level await” (MDN Web Docs). Retrieved September 18, 2024.
Use cases of top level await
Loading modules dynamically
const params = new URLSearchParams(location.search); const language = params.get('lang'); const messages = await import(`./messages-${language}.mjs`); // (A) console.log(messages.welcome);
In line A
, we dynamically import a module. Thanks to top-level await, that is almost as convenient as using a normal, static import.
Using a fallback if module loading fails
let mylib; try { mylib = await import('https://primary.example.com/mylib'); } catch { mylib = await import('https://secondary.example.com/mylib'); }
Using whichever resource loads fastest
const resource = await Promise.any([ fetch('http://example.com/first.txt') .then(response => response.text()), fetch('http://example.com/second.txt') .then(response => response.text()), ]);
Due to Promise.any()
, variable resource is initialized via whichever download finishes first.
“Use cases for top-level await
” (exploringjs.com). Retrieved September 18, 2024.
How does a module becomes asynchronous?
Consider the following two files.
first.mjs:
export let first; export const promise = (async () => { // (A) const response = await fetch('http://example.com/first.txt'); first = await response.text(); })();
main.mjs:
import {promise as firstPromise, first} from './first.mjs'; import {promise as secondPromise, second} from './second.mjs'; export const promise = (async () => { // (B) await Promise.all([firstPromise, secondPromise]); // (C) assert.equal(first, 'First!'); assert.equal(second, 'Second!'); })();
A module becomes asynchronous if:
- It directly uses top-level
await
(first.mjs). - It imports one or more asynchronous modules (main.mjs).
Each asynchronous module exports a Promise (line A and line B) that is fulfilled after its body was executed. At that point, it is safe to access the exports of that module.
In case (2), the importing module waits until the Promises of all imported asynchronous modules are fulfilled, before it enters its body (line C). Synchronous modules are handled as usually.
Awaited rejections and synchronous exceptions are managed as in async
functions.
“How does top-level await work under the hood?” (exploringjs.com). Retrieved September 18, 2024.
What are ther pros and cons of top level await
?
The two most important benefits of top-level await
are:
- It ensures that modules don’t access asynchronous imports before they are fully initialized.
- It handles asynchronicity transparently: Importers do not need to know if an imported module is asynchronous or not.
On the downside, top-level await
delays the initialization of importing modules. Therefore, it’s best used sparingly. Asynchronous tasks that take longer are better performed later, on demand.
However, even modules without top-level await can block importers (e.g. via an infinite loop at the top level), so blocking per se is not an argument against it.
“The pros and cons of top-level await” (exploringjs.com). Retrieved September 19, 2024.