Skip to content
aviral gupta

// B2.1 · ~34 min · Beginner

package.json and npm scripts

After this lesson you can create and read a package.json, run its scripts with npm, and decide when node --run is enough.

Lesson 1 of 5 in B2 npm and packages

Start of the module

You will be able to

  • Create package.json with npm init -y and read name, version, private, type, dependencies and devDependencies
  • Run scripts with npm run, npm test and npm start, pass arguments after --, and predict the pre and post scripts
  • Choose between npm run and node --run, knowing what node --run leaves out on purpose
  1. Warm-up · Activity 1 of 7

    Warm-up: you run npm init -y in an empty folder, with npm 11.19.0. Which of these fields are in the new package.json? Pick all that apply.

    Select all that apply.

  2. Predict · Activity 2 of 7

    Predict before you read on. This is package.json, and step.js prints the words it is given. You run npm run -s build (-s only hides npm's own > lines). What is printed, line by line?

    {
      "name": "demo",
      "version": "1.0.0",
      "scripts": {
        "prebuild": "node step.js pre",
        "build": "node step.js build",
        "postbuild": "node step.js post"
      }
    }
  3. Practice · Activity 3 of 7

    The "greet" script is node greet.js. Fill in the gap so that greet.js receives --name Ada, and npm does not take --name as its own option.

    npm run greet ____ --name Ada
    npm run greet --name Ada
  4. Practice · Activity 4 of 7

    Match each package.json entry to what it does.

  5. Practice · Activity 5 of 7

    In this project you type npm run dev. What happens?

    {
      "name": "demo",
      "version": "1.0.0",
      "scripts": {
        "start": "node main.js",
        "test": "node --test"
      }
    }
  6. Brain teaser · Activity 6 of 7

    Brain teaser. The same project as before: "prebuild", "build" and "postbuild" each run step.js. What does this command print?

    node --run build
  7. Apply · Activity 7 of 7

    Mini-task. In a new folder run npm init -y. In package.json, change "type" to "module", add "private": true, replace the "test" script with node --test and add a "pretest" script that runs echo checking. Write sum.js with an exported sum function and sum.test.js with one test. Run npm test, then node --run test, and compare the output.

    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

npm run and node --run, side by side

This program plays the part of your terminal. It writes a small project into a temporary folder: a "build" script with a "prebuild" and a "postbuild", each running step.js, which prints the words it gets and the npm_package_version variable. Then it runs the same command with npm run and with node --run and prints what each wrote to stdout. You need Node.js and npm, and no network.

main.js

// Plays the part of your terminal: makes a small project in a temporary
// folder and runs the same script with npm run and with node --run.
import {execSync} from "node:child_process";
import {mkdtempSync, writeFileSync} from "node:fs";
import {tmpdir} from "node:os";
import {join} from "node:path";

const dir = mkdtempSync(join(tmpdir(), "scripts-"));
const pkg = {
  name: "demo",
  version: "1.0.0",
  private: true,
  scripts: {
    prebuild: "node step.js prebuild",
    build: "node step.js build",
    postbuild: "node step.js postbuild"
  }
};
writeFileSync(join(dir, "package.json"), JSON.stringify(pkg, null, 2));
// step.js prints the words it got and the version that npm passes on.
writeFileSync(join(dir, "step.js"), 'console.log(process.argv.slice(2).join(" "), "| version:", process.env.npm_package_version);\n');

for (const command of ["npm run build -- --fast", "node --run build -- --fast"]) {
  console.log("$ " + command);
  console.log(execSync(command, {cwd: dir, encoding: "utf8"}).trim());
}

Run it with

node main.js

Output

$ npm run build -- --fast
> demo@1.0.0 prebuild
> node step.js prebuild

prebuild | version: 1.0.0

> demo@1.0.0 build
> node step.js build --fast

build --fast | version: 1.0.0

> demo@1.0.0 postbuild
> node step.js postbuild

postbuild | version: 1.0.0
$ node --run build -- --fast
build --fast | version: undefined
  • npm run prints > demo@1.0.0 and the command before each script; npm run -s would hide those lines.
  • --fast reaches build only: npm passes arguments after -- to the named script, not to prebuild or postbuild.
  • node --run ran build alone, and npm_package_version is undefined there: it sets no npm_ variables.
  • Both commands appended --fast to the same "build" entry of package.json.

Exercises

Exercise 1 of 2

What npm run will run

Write runOrder(pkg, name). pkg is a parsed package.json. Return the script names that npm run <name> runs, in order: pre<name> if it exists, then <name>, then post<name> if it exists. If "scripts" has no <name>, or there is no "scripts" at all, throw an Error whose message is Missing script: "<name>", as npm does. The last line of main.js prints the order for a small project.

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
  1. Hint 1

    Use pkg.scripts ?? {} so that a package.json without "scripts" does not crash your code.

  2. Hint 2

    name in scripts tells you whether a script exists. Throw first if it does not.

  3. Hint 3

    Build the three candidate names, "pre" + name, name and "post" + name, and keep only those that are in scripts.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

// The script names that npm run <name> runs, in order.
export function runOrder(pkg, name) {
  const scripts = pkg.scripts ?? {};
  if (!(name in scripts)) throw new Error('Missing script: "' + name + '"');
  return ["pre" + name, name, "post" + name].filter((script) => script in scripts);
}

const pkg = {scripts: {prebuild: "node clean.js", build: "node build.js", postbuild: "echo done"}};
console.log(runOrder(pkg, "build"));
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

// The script names that npm run <name> runs, in order.
export function runOrder(pkg, name) {
  return [name];
}

const pkg = {scripts: {prebuild: "node clean.js", build: "node build.js", postbuild: "echo done"}};
console.log(runOrder(pkg, "build"));

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {runOrder} from './main.js';

const pkg = {
  name: 'demo',
  scripts: {prebuild: 'node clean.js', build: 'node build.js', postbuild: 'echo done', test: 'node --test', posttest: 'echo tested', start: 'node main.js'}
};

test('runs prebuild, build and postbuild in that order', () => {
  const got = runOrder(pkg, 'build');
  assert.deepEqual(got, ['prebuild', 'build', 'postbuild'], `runOrder(pkg, "build") returned ${JSON.stringify(got)}`);
});

test('leaves out a pre or post script that does not exist', () => {
  const test = runOrder(pkg, 'test');
  assert.deepEqual(test, ['test', 'posttest'], `runOrder(pkg, "test") returned ${JSON.stringify(test)}`);
  const start = runOrder(pkg, 'start');
  assert.deepEqual(start, ['start'], `runOrder(pkg, "start") returned ${JSON.stringify(start)}`);
});

test('throws Missing script for a script that is not there', () => {
  assert.throws(() => runOrder(pkg, 'dev'), {message: 'Missing script: "dev"'}, 'runOrder(pkg, "dev") should throw an Error with the message Missing script: "dev"');
});

test('also throws when package.json has no "scripts" at all', () => {
  assert.throws(() => runOrder({name: 'empty'}, 'test'), {message: 'Missing script: "test"'}, 'runOrder({name: "empty"}, "test") should throw Missing script: "test"');
});

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

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 2

Check what npm init -y gave you

Write checkPackage(pkg), which returns a list of problems for a parsed package.json, in this order: 'type: set it to "module"' when "type" is not "module"; 'private: set it to true' when "private" is not true; 'test: write a real test script' when there is no "test" script or it is still the placeholder that npm init -y writes, kept in PLACEHOLDER. A package.json without problems 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
  1. Hint 1

    Check the three rules one after another with if, and push the message of each rule that fails.

  2. Hint 2

    pkg.private !== true is also true when "private" is missing.

  3. Hint 3

    pkg.scripts?.test is undefined when there is no "scripts" object, so !test covers both missing cases.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

// The "test" script that npm init -y writes.
const PLACEHOLDER = 'echo "Error: no test specified" && exit 1';

export function checkPackage(pkg) {
  const problems = [];
  if (pkg.type !== "module") problems.push('type: set it to "module"');
  if (pkg.private !== true) problems.push("private: set it to true");
  const test = pkg.scripts?.test;
  if (!test || test === PLACEHOLDER) problems.push("test: write a real test script");
  return problems;
}

console.log(checkPackage({name: "demo", version: "1.0.0", type: "commonjs", scripts: {test: PLACEHOLDER}}));
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

// The "test" script that npm init -y writes.
const PLACEHOLDER = 'echo "Error: no test specified" && exit 1';

export function checkPackage(pkg) {
  const problems = [];
  return problems;
}

console.log(checkPackage({name: "demo", version: "1.0.0", type: "commonjs", scripts: {test: PLACEHOLDER}}));

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {checkPackage} from './main.js';

// Exactly what npm init -y wrote with npm 11.19.0.
const fromInit = {
  name: 'demo',
  version: '1.0.0',
  description: '',
  main: 'index.js',
  scripts: {test: 'echo "Error: no test specified" && exit 1'},
  keywords: [],
  author: '',
  license: 'ISC',
  type: 'commonjs'
};

test('finds all three problems in what npm init -y writes', () => {
  const got = checkPackage(fromInit);
  assert.deepEqual(got, ['type: set it to "module"', 'private: set it to true', 'test: write a real test script'], `checkPackage(fromInit) returned ${JSON.stringify(got)}`);
});

test('returns an empty list for a finished package.json', () => {
  const got = checkPackage({name: 'demo', version: '1.0.0', private: true, type: 'module', scripts: {test: 'node --test'}});
  assert.deepEqual(got, [], `a finished package.json gave ${JSON.stringify(got)}`);
});

test('treats a missing test script like the placeholder', () => {
  const got = checkPackage({private: true, type: 'module'});
  assert.deepEqual(got, ['test: write a real test script'], `a package.json without scripts 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.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

A comma after the last entry of package.json

// package.json as it was saved, with a comma after the last script.
const text = `{
  "name": "demo",
  "scripts": {
    "start": "node main.js",
  }
}`;
const pkg = JSON.parse(text);
console.log(pkg.scripts.start);

What Node.js prints

SyntaxError: Expected double-quoted property name in JSON at position 66 (line 5 column 3)

Why, and the fix

package.json is JSON, not JavaScript: no comma after the last entry and no comments. JSON.parse rejects this text, and so does npm: npm start stops with npm error code EJSONPARSE and the note that package.json must be actual JSON. Delete the comma after "node main.js".

Relying on a pre script under node --run

// main.js, started by the "build" script. The "prebuild" script writes
// version.txt first, but only npm run build runs prebuild.
import {readFileSync} from "node:fs";

const version = readFileSync("version.txt", "utf8").trim();
console.log("building version", version);

What Node.js prints

Error: ENOENT: no such file or directory, open 'version.txt'

Why, and the fix

npm run build runs prebuild, which writes version.txt, before build. node --run build runs only build, so the file is missing, exactly as when you run node main.js by hand. Run such a project with npm run build, or make the step explicit in one script: "build": "node version.js && node main.js".

Reading npm_package_version under node --run

// main.js, started by "start": "node main.js". npm start sets
// npm_package_version; node --run start and node main.js do not.
const [major] = process.env.npm_package_version.split(".");
console.log("major version", major);

What Node.js prints

TypeError: Cannot read properties of undefined (reading 'split')

Why, and the fix

npm sets npm_package_name, npm_package_version and npm_lifecycle_event for its scripts. node --run leaves them out on purpose and sets only NODE_RUN_SCRIPT_NAME and NODE_RUN_PACKAGE_JSON_PATH, so the variable is undefined. To work however it is started, read the version from package.json itself instead of from the environment.

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.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

package.json describes the project

npm init -y writes a package.json without asking: "name" from the folder, "version" 1.0.0, a placeholder "test" script and, with npm 11.19.0, "type": "commonjs", the default of npm's init-type setting. "type" is read by Node.js: "module" makes .js files ES modules, as this course uses, so change it. "name" and "version" are required only to publish; "private": true makes npm refuse to publish at all. "dependencies" lists packages the program needs to run, "devDependencies" tools needed only while developing, such as a test library. package.json is strict JSON: no comments and no comma after the last entry.

npm run runs a script, with pre and post

npm run build runs the command stored under "build" in "scripts", in a shell, from the folder of package.json, with node_modules/.bin added to PATH. npm first prints > demo@1.0.0 build and the command; -s hides those lines. npm run alone lists the scripts. npm test and npm start are short for npm run test and npm run start; with no "start" script but a server.js, npm start runs node server.js. For every script, npm also runs pre<name> before it and post<name> after it when they exist. Arguments after -- reach the script only, not its pre or post script. Without --, npm takes --fast as its own option.

node --run is deliberately smaller

node --run build runs the same "build" entry and appends arguments after --. The Node.js docs list what it leaves out on purpose: pre and post scripts, and the package-manager environment variables. So npm_package_version and npm_lifecycle_event are undefined in the script; node --run sets NODE_RUN_SCRIPT_NAME instead. It prints no > lines and has no server.js fallback. A flag after the script name without -- goes to node itself: node --run build --fast stops with node: bad option: --fast. Use node --run for plain commands; use npm run when a project relies on pre or post scripts or npm_ variables.

Sources

Last reviewed September 30, 2026