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:
Consider the following two modules:
counter.mjsmain.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:
“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 modulesThe 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).pathnameIt 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:
import() were a function, we’d have to explicitly pass this information to it (e.g. via an parameter).“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:
await (first.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:
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.