Warm-up · Activity 1 of 7
// B2.2 · ~36 min · Beginner
Installing, package-lock.json and npm ci
After this lesson you can add and remove packages, read package-lock.json, reinstall a project exactly with npm ci, and run a package command with npx.
You will be able to
- Install and remove packages with npm install, --save-dev and npm uninstall, and say where they end up
- Read package-lock.json: version, resolved, integrity and dev, and why it is committed
- Reinstall exactly with npm ci, predict when it stops, and run package commands with npx
Predict · Activity 2 of 7
Predict before you read on. greet-1.2.0.tgz is a package made with npm pack. app/package.json has no "dependencies" yet. You run this command in app. What is "dependencies" afterwards?
npm install ../greet/greet-1.2.0.tgzPractice · Activity 3 of 7
check is a tool you use only while developing. Fill in the flag so that npm saves it in "devDependencies".
npm install ____ ../check/check-0.3.1.tgznpm install ../check/check-0.3.1.tgzPractice · Activity 4 of 7
Match each field of a package entry in package-lock.json to what it records.
Practice · Activity 5 of 7
What does the last command do?
npm install ../greet/greet-1.2.0.tgz rm package-lock.json npm ciBrain teaser · Activity 6 of 7
Brain teaser. greet is installed and in package-lock.json. Then someone adds "check": "file:../check/check-0.3.1.tgz" to "dependencies" by hand, in an editor, and does not run npm install. What does this command do?
npm ciApply · Activity 7 of 7
Mini-task, offline. Make a folder greet with package.json ("name": "greet", "version": "1.2.0", "type": "module", "exports": "./index.js") and an index.js that exports greet(name). Run npm pack in it. Next to it, make app with npm init -y and "type": "module", install ../greet/greet-1.2.0.tgz and import greet in main.js. Run it. Then delete node_modules, run npm ci and run main.js again.
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
What npm install records
This program plays the part of your terminal, and needs no network. It makes a tiny package, greet 1.2.0, packs it with npm pack, and installs the tarball into an app in a temporary folder. Then it prints what npm wrote into package.json and package-lock.json, and runs the app, which imports greet by its package name.
main.js
// Plays the part of your terminal: packs a tiny local package, installs it
// into an app and shows what npm recorded. No registry, no network.
import {execSync} from "node:child_process";
import {mkdirSync, mkdtempSync, readFileSync, writeFileSync} from "node:fs";
import {tmpdir} from "node:os";
import {join} from "node:path";
const root = mkdtempSync(join(tmpdir(), "deps-"));
const put = (file, text) => {
mkdirSync(join(root, file, ".."), {recursive: true});
writeFileSync(join(root, file), text);
};
const run = (command, dir) => execSync(command, {cwd: join(root, dir), encoding: "utf8"});
// A package of our own, greet 1.2.0, packed into greet-1.2.0.tgz.
put("greet/package.json", JSON.stringify({name: "greet", version: "1.2.0", type: "module", exports: "./index.js"}));
put("greet/index.js", 'export const greet = (name) => "Hello, " + name + "!";\n');
run("npm pack --silent", "greet");
// The app that installs it.
put("app/package.json", JSON.stringify({name: "app", version: "1.0.0", private: true, type: "module"}));
put("app/main.js", 'import {greet} from "greet";\nconsole.log(greet("Ada"));\n');
run("npm install --silent --no-audit --no-fund ../greet/greet-1.2.0.tgz", "app");
const pkg = JSON.parse(readFileSync(join(root, "app/package.json"), "utf8"));
const lock = JSON.parse(readFileSync(join(root, "app/package-lock.json"), "utf8"));
const entry = lock.packages["node_modules/greet"];
console.log("dependencies:", pkg.dependencies);
console.log("lockfileVersion:", lock.lockfileVersion);
console.log("locked:", entry.version, "from", entry.resolved);
console.log("integrity:", entry.integrity.slice(0, 7) + "...");
console.log(run("node main.js", "app").trim());
Run it with
node main.jsOutput
dependencies: { greet: 'file:../greet/greet-1.2.0.tgz' }
lockfileVersion: 3
locked: 1.2.0 from file:../greet/greet-1.2.0.tgz
integrity: sha512-...
Hello, Ada!- package.json keeps what you asked for: the path of the tarball.
- package-lock.json keeps what was installed: the exact version 1.2.0, where it came from and a sha512 hash of the file.
- import {greet} from "greet" works because npm unpacked the package into app/node_modules/greet.
- Delete app/node_modules and run npm ci in app: the same tree comes back from the lock.
Exercises
Exercise 1 of 2
The versions in a lock file
Write lockedVersions(lock), which takes a parsed package-lock.json and returns an object that maps each top-level package name to its locked version, such as {greet: "1.2.0"}. Top-level packages are the keys of lock.packages that start with node_modules/ and contain no further /node_modules/. Keep scoped names like @acme/log whole. Skip the project itself, the key "".
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
Object.entries(lock.packages) gives you [path, entry] pairs to loop over.
Hint 2
Skip paths that do not start with "node_modules/". Cut that prefix off to get the name.
Hint 3
A name that still contains "/node_modules/" belongs to a nested package: skip it. "@acme/log" contains a slash but no "/node_modules/", so it stays.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
// Maps each top-level package in a parsed package-lock.json to its version.
export function lockedVersions(lock) {
const versions = {};
for (const [path, entry] of Object.entries(lock.packages ?? {})) {
if (!path.startsWith("node_modules/")) continue;
const name = path.slice("node_modules/".length);
if (name.includes("/node_modules/")) continue;
versions[name] = entry.version;
}
return versions;
}
const lock = {lockfileVersion: 3, packages: {"": {name: "app"}, "node_modules/greet": {version: "1.2.0"}}};
console.log(lockedVersions(lock));
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
// Maps each top-level package in a parsed package-lock.json to its version.
export function lockedVersions(lock) {
const versions = {};
return versions;
}
const lock = {lockfileVersion: 3, packages: {"": {name: "app"}, "node_modules/greet": {version: "1.2.0"}}};
console.log(lockedVersions(lock));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {lockedVersions} from './main.js';
const lock = {
name: 'app',
lockfileVersion: 3,
packages: {
'': {name: 'app', version: '1.0.0'},
'node_modules/check': {version: '0.3.1', dev: true},
'node_modules/greet': {version: '1.2.0'},
'node_modules/@acme/log': {version: '2.0.0'},
'node_modules/greet/node_modules/tiny': {version: '0.1.0'}
}
};
test('maps each top-level package to its version', () => {
const got = lockedVersions(lock);
assert.deepEqual(got, {check: '0.3.1', greet: '1.2.0', '@acme/log': '2.0.0'}, `lockedVersions(lock) returned ${JSON.stringify(got)}`);
});
test('returns an empty object when nothing is installed', () => {
const got = lockedVersions({lockfileVersion: 3, packages: {'': {name: 'app'}}});
assert.deepEqual(got, {}, `a lock with only the project gave ${JSON.stringify(got)}`);
});
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
Would npm ci complain?
npm ci stops when package.json and the lock disagree. Write outOfSync(pkg, lock), a simple version of that check. Collect what package.json asks for in "dependencies" and "devDependencies", and what the root entry of the lock, lock.packages[""], recorded in the same two fields. Return the sorted names whose specifier differs, or that appear on only one side. In sync gives [].
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
Spread both fields into one object: {...pkg.dependencies, ...pkg.devDependencies}. Spreading undefined is fine.
Hint 2
Do the same with lock.packages[""], then collect all names from both objects in a Set.
Hint 3
Keep a name when asked[name] !== locked[name]; a missing side is undefined, so it counts as different. Sort the result.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
// Names that package.json and the root entry of package-lock.json disagree on.
export function outOfSync(pkg, lock) {
const root = lock.packages?.[""] ?? {};
const asked = {...pkg.dependencies, ...pkg.devDependencies};
const locked = {...root.dependencies, ...root.devDependencies};
const names = new Set([...Object.keys(asked), ...Object.keys(locked)]);
return [...names].filter((name) => asked[name] !== locked[name]).sort();
}
const pkg = {dependencies: {greet: "file:../greet/greet-1.2.0.tgz", check: "file:../check/check-0.3.1.tgz"}};
const lock = {packages: {"": {dependencies: {greet: "file:../greet/greet-1.2.0.tgz"}}}};
console.log(outOfSync(pkg, lock));
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
// Names that package.json and the root entry of package-lock.json disagree on.
export function outOfSync(pkg, lock) {
return [];
}
const pkg = {dependencies: {greet: "file:../greet/greet-1.2.0.tgz", check: "file:../check/check-0.3.1.tgz"}};
const lock = {packages: {"": {dependencies: {greet: "file:../greet/greet-1.2.0.tgz"}}}};
console.log(outOfSync(pkg, lock));
main.test.js
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {outOfSync} from './main.js';
const GREET = 'file:../greet/greet-1.2.0.tgz';
const CHECK = 'file:../check/check-0.3.1.tgz';
const lock = {lockfileVersion: 3, packages: {'': {name: 'app', dependencies: {greet: GREET}, devDependencies: {check: CHECK}}}};
test('returns [] when package.json and the lock agree', () => {
const got = outOfSync({dependencies: {greet: GREET}, devDependencies: {check: CHECK}}, lock);
assert.deepEqual(got, [], `outOfSync returned ${JSON.stringify(got)} for matching files`);
});
test('names a dependency added to package.json by hand', () => {
const got = outOfSync({dependencies: {greet: GREET, extra: 'file:../extra'}, devDependencies: {check: CHECK}}, lock);
assert.deepEqual(got, ['extra'], `outOfSync returned ${JSON.stringify(got)}`);
});
test('names a dependency whose specifier changed or that was removed', () => {
const got = outOfSync({dependencies: {greet: 'file:../greet/greet-1.3.0.tgz'}}, lock);
assert.deepEqual(got, ['check', 'greet'], `outOfSync returned ${JSON.stringify(got)}; expected both names, sorted`);
});
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
Running a fresh clone before installing
// main.js of a project you just cloned. package.json lists greet,
// but node_modules is not in the repository.
import {greet} from "greet";
console.log(greet("Ada"));
What Node.js prints
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'greet' imported fromWhy, and the fix
A bare name like greet is looked up in node_modules, and node_modules is not committed. After cloning, run npm ci, which installs exactly what package-lock.json records, or npm install when there is no lock yet. Then run the program again.
Looking a package up by its name in "packages"
const lock = {
name: "app",
version: "1.0.0",
lockfileVersion: 3,
requires: true,
packages: {
"": {
name: "app",
version: "1.0.0",
dependencies: {greet: "file:../greet/greet-1.2.0.tgz"},
devDependencies: {check: "file:../check/check-0.3.1.tgz"}
},
"node_modules/check": {version: "0.3.1", resolved: "file:../check/check-0.3.1.tgz", integrity: "sha512-4izY", dev: true},
"node_modules/greet": {version: "1.2.0", resolved: "file:../greet/greet-1.2.0.tgz", integrity: "sha512-4I5b"}
}
};
// The keys of "packages" are paths, not names.
console.log(lock.packages["greet"].version);
What Node.js prints
TypeError: Cannot read properties of undefined (reading 'version')Why, and the fix
The keys of "packages" are folder paths: "" for the project, "node_modules/greet" for greet. lock.packages["greet"] is undefined, so reading .version from it fails. Use lock.packages["node_modules/greet"].version, which is "1.2.0".
Reading the old "dependencies" section
const lock = {
name: "app",
version: "1.0.0",
lockfileVersion: 3,
requires: true,
packages: {
"": {
name: "app",
version: "1.0.0",
dependencies: {greet: "file:../greet/greet-1.2.0.tgz"},
devDependencies: {check: "file:../check/check-0.3.1.tgz"}
},
"node_modules/check": {version: "0.3.1", resolved: "file:../check/check-0.3.1.tgz", integrity: "sha512-4izY", dev: true},
"node_modules/greet": {version: "1.2.0", resolved: "file:../greet/greet-1.2.0.tgz", integrity: "sha512-4I5b"}
}
};
// Code written for old lock files reads lock.dependencies.
console.log(Object.keys(lock.dependencies));
What Node.js prints
TypeError: Cannot convert undefined or null to objectWhy, and the fix
The "dependencies" section is legacy data for lockfileVersion 1. npm 9 and later write lockfileVersion 3, which has only "packages", so lock.dependencies is undefined and Object.keys cannot take it. Read lock.packages and its "node_modules/..." keys instead.
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.