Warm-up · Activity 1 of 7
// B2.4 · ~34 min · Beginner
import/export, import.meta.dirname, node: built-ins
After this lesson you can split a program into ES modules that share what they export, load modules and JSON files when you need them, and locate files next to a module.
You will be able to
- Write named and default exports and import them with a full relative specifier
- Load modules with import() and JSON with with {type: "json"}, and predict live bindings
- Use import.meta.dirname and import.meta.filename, and know which built-ins need node:
Predict · Activity 2 of 7
Predict before you read on. main.js imports count and increment from counter.js and calls increment twice. What does node main.js print?
// counter.js export let count = 0; export function increment() { count += 1; } // main.js import {count, increment} from "./counter.js"; increment(); increment(); console.log(count);Practice · Activity 3 of 7
cart.js sits next to main.js. Fill in the rest of the specifier so that Node.js finds it.
import {add} from "./cart____";import {add} from "./cart";Practice · Activity 4 of 7
cart.js exports add and count by name and total as its default. Match each import to what it gives main.js.
Practice · Activity 5 of 7
The file /home/ada/shop/src/main.js contains this line. You are in /home/ada/shop and run node src/main.js. What does it print?
console.log(import.meta.dirname);Brain teaser · Activity 6 of 7
Brain teaser. main.js loads greet.js with import(). What does it print?
// greet.js export default function greet(name) { return "Hello, " + name + "!"; } // main.js const greeting = await import("./greet.js"); console.log(typeof greeting, typeof greeting.default);Apply · Activity 7 of 7
Mini-task. In a folder whose package.json has "type": "module", write lib/money.js with a named export CURRENCY and a default export money(amount) that formats 3.5 as 3.50 EUR. Add settings.json with a shop name. In main.js, import both from lib/money.js, import settings.json with its import attribute, and print the shop name and money(3.5). Print basename(import.meta.filename) with node:path. Finally load lib/money.js again with import() and check that its default is the same function.
Check your work against this list
Build it yourself
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
A cart in its own module
cart.js keeps a count and a list of items and exports add and count by name, and total as its default. main.js imports all three, adds two items and reads count before and after: the import follows the variable in cart.js. format.js has a default export too, which main.js names money. At the end, import * as cart shows every export of cart.js in one object. It runs in the browser too.
main.js
import total, {add, count} from "./cart.js";
import money from "./format.js";
import * as cart from "./cart.js";
console.log("items at start:", count);
add("tea", 3.5);
add("cake", 4);
console.log("items now:", count);
console.log("total:", money(total()));
console.log("cart.js exports:", Object.keys(cart).join(", "));
console.log("same count:", cart.count === count);
cart.js
// A tiny shopping cart. Its state lives in this module.
export let count = 0;
const items = [];
export function add(name, price) {
items.push({name, price});
count += 1;
}
export default function total() {
return items.reduce((sum, item) => sum + item.price, 0);
}
format.js
export default function format(amount) {
return amount.toFixed(2) + " EUR";
}
Run it with
node main.jsOutput
items at start: 0
items now: 2
total: 7.50 EUR
cart.js exports: add, count, default
same count: true- count is 0, then 2: main.js reads the live variable of cart.js, not a copy.
- format.js calls its function format, but main.js imports the default export as money: the importer picks the name.
- The module object holds the default export under the key default, next to the named ones.
Change it and run it
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Exercises
Exercise 1 of 3
Named and default exports
rates.js is given: it exports STANDARD (0.19) and REDUCED (0.07) by name, and round2 as its default. In main.js, import all three. Then export a default function gross(net, reduced = false) that adds tax at the matching rate and rounds with round2, so gross(10) is 11.9 and gross(10, true) is 10.7. Also export rates, an object {standard, reduced} with both rates, by name.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.
Hints
Hint 1
One line imports both kinds: import round2, {STANDARD, REDUCED} from "./rates.js";
Hint 2
net * (1 + STANDARD) is the gross amount; pass it to round2.
Hint 3
Pick the rate with reduced ? REDUCED : STANDARD, and build rates as {standard: STANDARD, reduced: REDUCED}.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
import round2, {STANDARD, REDUCED} from "./rates.js";
export default function gross(net, reduced = false) {
return round2(net * (1 + (reduced ? REDUCED : STANDARD)));
}
export const rates = {standard: STANDARD, reduced: REDUCED};
Run it on your computer
Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
// Import round2 (default) and STANDARD, REDUCED (named) from ./rates.js here.
export default function gross(net, reduced = false) {
return net;
}
export const rates = {};
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import * as main from './main.js';
test('the default export is a function', () => {
assert.equal(typeof main.default, 'function', `the default export is ${typeof main.default}`);
});
test('adds the standard rate and rounds to cents', () => {
assert.equal(main.default(10), 11.9, `gross(10) returned ${main.default(10)}`);
assert.equal(main.default(19.99), 23.79, `gross(19.99) returned ${main.default(19.99)}`);
});
test('adds the reduced rate when asked', () => {
assert.equal(main.default(10, true), 10.7, `gross(10, true) returned ${main.default(10, true)}`);
});
test('exports rates by name', () => {
assert.deepEqual(main.rates, {standard: 0.19, reduced: 0.07}, `rates is ${JSON.stringify(main.rates)}`);
});
rates.js
// Tax rates as fractions, and a helper that rounds to cents.
export const STANDARD = 0.19;
export const REDUCED = 0.07;
export default function round2(amount) {
return Math.round(amount * 100) / 100;
}
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 2 of 3
Load a module by name
greet-en.js and greet-de.js each have a default export greet(name). Write the async function greet(lang, name) in main.js: it loads "./greet-" + lang + ".js" with import() and returns what that module's default export gives for name, so greet("de", "Ada") is "Hallo, Ada!". When no such module exists, import() rejects: catch that and return "No greeting for " + lang. Run the tests with node --test.
This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.
Hints
Hint 1
import() takes any string, so you can build the specifier: "./greet-" + lang + ".js".
Hint 2
await the import(); the function is on the result under .default.
Hint 3
Wrap the import in try/catch: a missing module makes the awaited import() throw.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
// Loads ./greet-<lang>.js when it is needed and greets name with its default export.
export async function greet(lang, name) {
try {
const module = await import("./greet-" + lang + ".js");
return module.default(name);
} catch {
return "No greeting for " + lang;
}
}
Run it on your computer
Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
// Loads ./greet-<lang>.js when it is needed and greets name with its default export.
export async function greet(lang, name) {
return "";
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {greet} from './main.js';
test('greets in English with greet-en.js', async () => {
const got = await greet('en', 'Ada');
assert.equal(got, 'Hello, Ada!', `greet('en', 'Ada') returned ${JSON.stringify(got)}`);
});
test('greets in German with greet-de.js', async () => {
const got = await greet('de', 'Ada');
assert.equal(got, 'Hallo, Ada!', `greet('de', 'Ada') returned ${JSON.stringify(got)}`);
});
test('says so when there is no module for the language', async () => {
const got = await greet('fr', 'Ada');
assert.equal(got, 'No greeting for fr', `greet('fr', 'Ada') returned ${JSON.stringify(got)}`);
});
greet-en.js
export default function greet(name) {
return "Hello, " + name + "!";
}
greet-de.js
export default function greet(name) {
return "Hallo, " + name + "!";
}
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsExercise 3 of 3
JSON settings next to the module
settings.json sits next to main.js. Import it with its import attribute, and write two functions. banner() returns the name and the version from the file, separated by a space: "shop 1.4.2". settingsPath() returns the absolute path of settings.json, built with join from node:path and import.meta.dirname, so it is right whichever folder you run node from. Run the tests with node --test.
This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.
Hints
Hint 1
import settings from "./settings.json" with {type: "json"}; gives you the parsed object.
Hint 2
settings.name + " " + settings.version builds the banner.
Hint 3
import {join} from "node:path"; then join(import.meta.dirname, "settings.json").
Show a solution
One way to solve it. Yours can look different and still pass the checks.
import {join} from "node:path";
import settings from "./settings.json" with {type: "json"};
export function banner() {
return settings.name + " " + settings.version;
}
export function settingsPath() {
return join(import.meta.dirname, "settings.json");
}
Run it on your computer
Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
// Import settings.json (it needs an import attribute) and join from node:path here.
export function banner() {
return "";
}
export function settingsPath() {
return "";
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {banner, settingsPath} from './main.js';
test('banner() shows the name and version from settings.json', () => {
assert.equal(banner(), 'shop 1.4.2', `banner() returned ${JSON.stringify(banner())}`);
});
test('settingsPath() is settings.json in the folder of main.js', () => {
const got = settingsPath();
const folder = import.meta.dirname;
assert.ok(got.startsWith(folder) && got.endsWith('settings.json') && got.length === folder.length + 'settings.json'.length + 1, `settingsPath() returned ${JSON.stringify(got)}; expected settings.json inside ${folder}`);
});
settings.json
{"name": "shop", "version": "1.4.2"}
package.json
{
"type": "module"
}
package.json tells Node.js that the .js files are modules; keep it in the folder.
Run the program:
node main.jsRun the checks (needs learnrun.js in the same folder):
node --testDownload learnrun.jsCommon mistakes
Leaving out the extension
import {add} from "./cart";
add("tea", 3.5);
What Node.js prints
Error [ERR_MODULE_NOT_FOUND]: Cannot find moduleWhy, and the fix
In an ES module Node.js takes a relative specifier literally: it looks for a file called cart, finds none, and adds Did you mean to import "./cart.js"? under the error. Write the full name, import {add} from "./cart.js". The same goes for folders: import "./lib/index.js", not "./lib".
Braces around a default export
import {round2} from "./rates.js";
console.log(round2(12.345));
What Node.js prints
SyntaxError: The requested module './rates.js' does not provide an export named 'round2'Why, and the fix
round2 is the default export of rates.js. Braces ask for a named export, and rates.js has only STANDARD and REDUCED by name. Import the default without braces, import round2 from "./rates.js", or both at once: import round2, {STANDARD} from "./rates.js". The check happens before any line of main.js runs.
Importing JSON without its attribute
import settings from "./settings.json";
console.log(settings.name);
What Node.js prints
TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: ModuleWhy, and the fix
The full message ends with needs an import attribute of "type: json". Node.js requires the attribute for every JSON module, so a file cannot pass itself off as JavaScript: import settings from "./settings.json" with {type: "json"}. With import(), pass it as a second argument: await import("./settings.json", {with: {type: "json"}}).
JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source
Exit ticket
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.