Working with ES6 modules Flashcards

1
Q

What are named exports?

A

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}`;
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

What are named imports?

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

What are namespace imports?

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

What is a default export?

A

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);
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

What are dynamic imports?

A

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
});
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

What is the syntax of static imports?

A
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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

What is a module specifier?

A

The string after from is called a module specifier. It identifies from which module we want to import.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

What are the various types of module specifiers?

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

What is a default import?

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

What is re-exporting?

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

Explain why imports are read-only views on exports

A

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++).

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

List ESM and commonJs module specifier differences

A

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.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

List and explain Node.js supported module extensions

A

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.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

What does import.meta hold?

A

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 well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q

How can you get a URL instance that points to a file data.txt that sits next to the current module?

A

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);
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q

How can you convert between file: URLs and paths?

A

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.

17
Q

How can you load a module dynamically?

A

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.

18
Q

Why is import() an operator and not a function?

A

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.
19
Q

What is a top level await?

A

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.

20
Q

Use cases of top level await

A

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.

21
Q

How does a module becomes asynchronous?

A

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:

  1. It directly uses top-level await (first.mjs).
  2. 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.

22
Q

What are ther pros and cons of top level await?

A

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.