Warm-up · Activity 1 of 7
Warm-up from lesson B4.4 on scope. What does this print?
{
const secret = 42;
}
console.log(typeof secret);// I3.1 · ~34 min · Intermediate
After this lesson you split a program into several files: each file exports what others may use, imports what it needs, and keeps everything else private.
Lesson 1 of 5 in I3 Modules, Node.js and npm
You will be able to
Warm-up · Activity 1 of 7
{
const secret = 42;
}
console.log(typeof secret);Predict · Activity 2 of 7
// counter.js
console.log("counter.js runs");
export let count = 0;
// logger.js
import {count} from "./counter.js";
export const start = count;
// main.js
import {count} from "./counter.js";
import {start} from "./logger.js";
console.log("main runs");Practice · Activity 3 of 7
____ const VAT = 0.19;Practice · Activity 4 of 7
Practice · Activity 5 of 7
// math.js
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// main.js
import {add, multiply} from "./math.js";
console.log(multiply(add(1, 2), 4));Brain teaser · Activity 6 of 7
// setup.js
console.log("setup");
// main.js
console.log("main");
import "./setup.js";Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
prices.js exports withVat and currency and keeps VAT private. counter.js exports count and increment, and logs when it runs. logger.js imports counter.js too. main.js imports from all three. The output shows that counter.js ran once, that main.js cannot see VAT, that count is a live binding which both increment() calls changed, and that main.js cannot assign to it.
main.js
import {withVat, currency} from "./prices.js";
import {count, increment} from "./counter.js";
import {log} from "./logger.js"; // logger.js imports counter.js too
console.log(withVat(10), currency);
console.log(typeof VAT); // VAT belongs to prices.js
increment();
log("hello");
console.log("count:", count); // a live binding: 2
try {
count = 10;
} catch (error) {
console.log(error.name + ":", error.message);
}
prices.js
const VAT = 0.19; // not exported: private to this file
export function withVat(net) {
return Math.round(net * (1 + VAT) * 100) / 100;
}
export const currency = "EUR";
counter.js
console.log("counter.js runs");
export let count = 0;
export function increment() {
count += 1;
}
logger.js
import {increment} from "./counter.js";
export function log(message) {
increment();
console.log("log:", message);
}
Run it with
node main.jsOutput
counter.js runs
11.9 EUR
undefined
log: hello
count: 2
TypeError: Assignment to constant variable.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.
Exercise 1 of 2
main.js is a module that other files will import. It should export exactly two functions: slugify(title), which turns "Hello World!" into "hello-world", and isSlug(text). The helper clean(text) is only used inside the file and must stay private. The starter exports clean too. Remove that export, so the module’s public API is just the two functions.
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.
Run the tests: the module exports three names. Which one is only a helper?
A function without export is still usable inside its own file.
Delete the word export in front of function clean.
One way to solve it. Yours can look different and still pass the checks.
function clean(text) {
return text.toLowerCase().replace(/[^a-z0-9 ]/g, "").trim();
}
export function slugify(title) {
return clean(title).split(/ +/).join("-");
}
export function isSlug(text) {
return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(text);
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
export function clean(text) {
return text.toLowerCase().replace(/[^a-z0-9 ]/g, "").trim();
}
export function slugify(title) {
return clean(title).split(/ +/).join("-");
}
export function isSlug(text) {
return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(text);
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import * as slugs from './main.js';
test('The module exports exactly slugify and isSlug', () => {
const names = Object.keys(slugs).sort();
assert.deepEqual(names, ['isSlug', 'slugify'], `main.js exports ${JSON.stringify(names)}`);
});
test('slugify turns a title into a slug', () => {
const got = slugs.slugify('Hello World!');
assert.equal(got, 'hello-world', `slugify gave ${got}`);
});
test('isSlug accepts a slug and rejects a title', () => {
assert.equal(slugs.isSlug('hello-world'), true, 'hello-world is a slug');
assert.equal(slugs.isSlug('Hello World'), false, 'Hello World is not a slug');
});
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 2
main.js is a small store module. It should export addItem(name) and count, the number of items so far, so that an importer sees count change after each addItem. The starter exports count as items.length, worked out once when the module ran, so it stays 0. Make count a let that addItem updates.
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.
Run the tests: count stays 0. When is items.length read in the starter?
An imported let is a live binding: when main.js assigns a new value, importers see it.
export let count = 0; and in addItem: count = items.length;
One way to solve it. Yours can look different and still pass the checks.
const items = [];
export let count = 0;
export function addItem(name) {
items.push(name);
count = items.length;
}
Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.js
const items = [];
export function addItem(name) {
items.push(name);
}
export const count = items.length;
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {addItem, count} from './main.js';
import * as store from './main.js';
test('count starts at 0', () => {
assert.equal(store.count, 0, `count is ${store.count} before any item`);
});
test('The imported count follows addItem', () => {
addItem('tea');
addItem('cake');
assert.equal(count, 2, `after two items the imported count is ${count}`);
});
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.jsimport {sum} from "./math.js";
console.log(sum(1, 2));
What Node.js prints
SyntaxError: The requested module './math.js' does not provide an export named 'sum'Why, and the fix
The names in the braces must be exactly the exported names. math.js exports add, not sum, so the module cannot be linked and nothing runs. Import add, or rename while importing: import {add as sum} from "./math.js". The error is a SyntaxError although the syntax is fine: it happens while the modules are linked, before any code runs.
import {count} from "./counter.js";
count = count + 1;
console.log(count);
What Node.js prints
TypeError: Assignment to constant variable.Why, and the fix
An import is read-only in the importing file, even if the exporting module declared it with let. Only counter.js can change count. Export a function that does it, such as export function increment() { count += 1; }, and call that.
import {add} from "./math";
console.log(add(1, 2));
What Node.js prints
Error [ERR_MODULE_NOT_FOUND]: Cannot find moduleWhy, and the fix
Node.js does not try extensions for an ES module path: "./math" looks for a file called exactly math, which does not exist. A browser would request the URL ./math and fail the same way. Write the full file name: "./math.js".
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
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.