Warm-up · Activity 1 of 7
Warm-up from lesson I3.3. package.json says "module". What does node tools.cjs print?
// package.json
{"type": "module"}
// tools.cjs
const path = require("node:path");
console.log(path.basename("/docs/notes.txt"));// I3.4 · ~36 min · Intermediate
After this lesson you can add packages to a project with npm, use them by name in your code, and run the project’s tasks as npm scripts.
You will be able to
Warm-up · Activity 1 of 7
// package.json
{"type": "module"}
// tools.cjs
const path = require("node:path");
console.log(path.basename("/docs/notes.txt"));Predict · Activity 2 of 7
npm install tiny-casePractice · Activity 3 of 7
npm install ____ say-hiPractice · Activity 4 of 7
Practice · Activity 5 of 7
{
"scripts": {
"greet": "say-hi there"
}
}Brain teaser · Activity 6 of 7
// package.json
{
"name": "app",
"version": "1.0.0",
"scripts": {
"posttest": "node step.js post",
"test": "node step.js test",
"pretest": "node step.js pre"
}
}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
This project ran npm install tiny-case, so package.json lists "tiny-case": "^2.1.0" under dependencies, and the package sits in node_modules/tiny-case. main.js imports it by its bare name. The two files of the package are shown too, so you can see what Node.js finds there: its package.json says, through "exports", which file to load.
main.js
// npm install tiny-case put the package into node_modules/tiny-case.
// A bare name (no ./) is looked up in node_modules:
import {kebab} from "tiny-case";
const titles = ["Hello World", " ES Modules in Node "];
for (const title of titles) {
console.log(kebab(title));
}
node_modules/tiny-case/package.json
{"name": "tiny-case", "version": "2.1.0", "type": "module", "exports": "./index.js"}
node_modules/tiny-case/index.js
export const kebab = (text) => text.trim().toLowerCase().split(/\s+/).join("-");
Run it with
node main.jsOutput
hello-world
es-modules-in-nodeExercise 1 of 2
addDependency(pkg, name, version, options) returns a new package.json object with the package added, as npm install would save it: under devDependencies if options.dev is true, else under dependencies; with the exact version if options.exact is true, else with ^ in front. It must not change pkg. The starter ignores both options.
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.
First choose the field name from options.dev, then the range from options.exact.
A computed key, [field]: …, puts the new object under the chosen field.
Spread pkg[field] into a new object so the original is not changed.
One way to solve it. Yours can look different and still pass the checks.
export function addDependency(pkg, name, version, options = {}) {
const field = options.dev ? "devDependencies" : "dependencies";
const range = options.exact ? version : "^" + version;
return {...pkg, [field]: {...pkg[field], [name]: range}};
}
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 addDependency(pkg, name, version, options = {}) {
const dependencies = {...pkg.dependencies, [name]: "^" + version};
return {...pkg, dependencies};
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {addDependency} from './main.js';
const base = {name: 'app', version: '1.0.0', dependencies: {'tiny-case': '^2.1.0'}};
test('A package goes under dependencies with a caret', () => {
const pkg = addDependency(base, 'chalk', '5.6.2');
assert.deepEqual(pkg.dependencies, {'tiny-case': '^2.1.0', chalk: '^5.6.2'}, `dependencies: ${JSON.stringify(pkg.dependencies)}`);
});
test('With dev it goes under devDependencies', () => {
const pkg = addDependency(base, 'say-hi', '1.0.3', {dev: true});
assert.deepEqual(pkg.devDependencies, {'say-hi': '^1.0.3'}, `devDependencies: ${JSON.stringify(pkg.devDependencies)}`);
assert.deepEqual(pkg.dependencies, {'tiny-case': '^2.1.0'}, `dependencies: ${JSON.stringify(pkg.dependencies)}`);
});
test('With exact the version has no caret', () => {
const pkg = addDependency(base, 'chalk', '5.6.2', {exact: true});
assert.equal(pkg.dependencies.chalk, '5.6.2', `chalk: ${pkg.dependencies.chalk}`);
});
test('The original object stays as it was', () => {
addDependency(base, 'say-hi', '1.0.3', {dev: true});
assert.deepEqual(base, {name: 'app', version: '1.0.0', dependencies: {'tiny-case': '^2.1.0'}}, `base: ${JSON.stringify(base)}`);
});
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
runOrder(scripts, name, args) returns the command lines npm run name -- …args would start, in order: the pre script if there is one, then the script itself with the arguments added, then the post script if there is one. The arguments go only to the script itself. If the script does not exist, throw an Error with the message Missing script: "name". The starter only returns the script.
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.
Object.hasOwn(scripts, "pre" + name) tells you whether a pre script exists.
Add the arguments only to the middle command: [scripts[name], ...args].join(" ").
Check the name first and throw before you build the list.
One way to solve it. Yours can look different and still pass the checks.
export function runOrder(scripts, name, args = []) {
if (!Object.hasOwn(scripts, name)) {
throw new Error('Missing script: "' + name + '"');
}
const order = [];
if (Object.hasOwn(scripts, "pre" + name)) order.push(scripts["pre" + name]);
order.push([scripts[name], ...args].join(" "));
if (Object.hasOwn(scripts, "post" + name)) order.push(scripts["post" + name]);
return order;
}
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 runOrder(scripts, name, args = []) {
return [[scripts[name], ...args].join(" ")];
}
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runOrder} from './main.js';
const scripts = {prebuild: 'node clean.js', build: 'node build.js', postbuild: 'node zip.js', test: 'node --test'};
test('A script without pre or post runs alone', () => {
const order = runOrder(scripts, 'test');
assert.deepEqual(order, ['node --test'], `order: ${JSON.stringify(order)}`);
});
test('Pre and post scripts run around the script', () => {
const order = runOrder(scripts, 'build');
assert.deepEqual(order, ['node clean.js', 'node build.js', 'node zip.js'], `order: ${JSON.stringify(order)}`);
});
test('Arguments go only to the script itself', () => {
const order = runOrder(scripts, 'build', ['--watch']);
assert.deepEqual(order, ['node clean.js', 'node build.js --watch', 'node zip.js'], `order: ${JSON.stringify(order)}`);
});
test('A missing script throws Missing script', () => {
assert.throws(() => runOrder(scripts, 'biuld'), {message: 'Missing script: "biuld"'}, 'runOrder must throw for a name that is not in scripts');
});
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 {kebab} from "tiny-case";
console.log(kebab("Hello World"));
What Node.js prints
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'tiny-case' imported fromWhy, and the fix
package.json may list tiny-case, but the package is not in node_modules: after a fresh git clone, node_modules is empty. Run npm install (or npm ci) first. The same error appears for a typo in the package name.
import {kebab} from "tiny-case/index.js";
console.log(kebab("Hello World"));
What Node.js prints
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './index.js' is not defined by "exports"Why, and the fix
The package’s "exports" field lists what it lets you import, and its own index.js path is not on that list. Import the package by its name: import {kebab} from "tiny-case". Paths into a package can change in any update; the exported names are what the package promises.
import kebab from "tiny-case";
console.log(kebab("Hello World"));
What Node.js prints
SyntaxError: The requested module 'tiny-case' does not provide an export named 'default'Why, and the fix
tiny-case is an ES module that exports kebab by name and has no default export. Use braces: import {kebab} from "tiny-case". A package’s README shows how to import it; if it says import x from …, it has a default export.
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.