
# npm Scripts

`npm` reads the scripts in the workspace's `package.json` and turns each one into a task: a name,
the package manager as its source, and the script's own command line as its detail.

```ts
import { Workbench, FileSystem } from "codelet/workbench";
import { npm } from "codelet/extensions/npm";
import { terminal } from "codelet/extensions/terminal";
import { justBash } from "codelet/extensions/terminal/just-bash";

new Workbench({
  parent: document.getElementById("app")!,
  fs: new FileSystem({
    "/package.json": JSON.stringify({ scripts: { build: "tsc", test: "vitest run" } }),
  }),
  extensions: [npm(), terminal(), justBash()],
});
```

::warning
A page cannot spawn a process, so running a script means typing its command line into a terminal
another extension contributed. `npm` needs
[`codelet/extensions/terminal`](/extensions/terminal) plus a shell mounted alongside it —
[`just-bash`](/extensions/just-bash) or [`webcontainer`](/extensions/webcontainer). Mount `npm`
with no shell in the workbench and every task refuses to run: the reader sees an error message,
and `executeTask` rejects.

`just-bash` opens a shell, but it has no package manager installed in it — a script run there
fails with "command not found" rather than actually building anything. `webcontainer` is real
node with `npm`, `pnpm`, `yarn` and `bun` all on its `PATH`. If your workbench mounts both, tell
`npm` which one to use (see [Options](#options)) — the shells are not interchangeable for this.
::

## Options

| Option  | Type     | Default | Description                                                                                             |
| ------- | -------- | ------- | ------------------------------------------------------------------------------------------------------- |
| `shell` | `string` | —       | The id of a `contributes.terminal.profiles` entry to run scripts in, `"webcontainer.shell"` among them. |

```ts
npm({ shell: "webcontainer.shell" });
```

Without `shell`, the last contributed shell profile is used — the shell a host mounted last is
taken as the more deliberate choice. A named shell that isn't offered falls back the same way, so
naming `webcontainer.shell` is safe even before cross-origin isolation makes it available: the
task runs on whatever shell is there once it is.

## Choosing a package manager

The manager for a task's command line is worked out from the workspace, in order:

1. The `packageManager` field in `package.json`, if the tree declares one (`"pnpm@11.20.0"` reads
   as `pnpm`).
2. Otherwise, whichever lockfile is at the workspace root: `pnpm-lock.yaml`, `yarn.lock`,
   `bun.lock` or `bun.lockb`, then `package-lock.json`, first match wins.
3. `npm`, if none of the above is there.

Each task's command line is `<manager> install && <manager> run <script>` — install runs first,
every time. A shell in a page is either a container that just booted or a tree that arrived over
a tarball, so `node_modules` is rarely already there, and every manager's install is close to a
no-op once it is.

## What it adds

One `vscode.Task` per script in `package.json`, plus an `npm: Run Script` command in the
command palette that lists them in a quick pick and runs the one you choose. Nothing is held
between fetches: `package.json` is read fresh each time, so a script you just added or renamed
shows up the next time you open the palette or the Tasks pane — no reload needed. A workspace
with no `package.json`, or one that fails to parse, simply has no npm tasks.

A script named exactly `build`, `test` or `clean` is tagged with the matching task group; any
other name is left ungrouped.

## Running one

Mount [`codelet/extensions/tasks`](/extensions/tasks) for a pane listing every script with a
click to run it, or reach for the `npm: Run Script` command in the palette. Both go through the
same `vscode.tasks` API, so a second extension that also provides `"npm"`-typed tasks shows up
alongside these in either place.

:read-more{to="/api/integrations"}
