pnpm 07 📦 Running Scripts and Executing Binaries with pnpm run and pnpm dlx
A project’s package.json is both a manifest and a command registry. The scripts field defines named commands that can be invoked with a short alias, and pnpm provides a resolution layer that decides whether a given name refers to a script, a taskfile, or a binary from a dependency. In pnpm, the run keyword is optional—pnpm test is the same as pnpm run test—and the resolution order is explicit. For tools that are needed only once, pnpm dlx installs the package in a temporary environment, runs the binary, and discards it without polluting the project’s dependency tree.
Key point: pnpm <name> resolves the name in a fixed order: local script, dependency binary, shell command. If you use pnpm run <name>, it strictly resolves against the scripts field. The pnpm dlx command is the equivalent of npx, but with pnpm’s caching and strictness.
Why script execution needs a resolution layer
The naming collision problem. A project may have a script called test and a dependency that exposes a binary called test. The scripts field in package.json takes precedence for pnpm run, but for bare pnpm <name>, pnpm checks the scripts first, then the binaries. If you want to bypass the scripts and execute the binary directly, you can use pnpm exec <bin>.
The workspace context problem. In a monorepo, a script defined in a workspace runs from the directory of that workspace, not from the repository root. This is deliberate: scripts often expect to read files relative to their own package. The --filter flag or pnpm -r run executes the script across multiple workspaces.
The binary forwarding problem. A dependency binary may need Node.js-specific flags. pnpm provides --node-options to forward flags to the Node.js process running the binary. Without this, debugging a binary installed as a dependency would require knowing its exact path and invoking Node.js manually.
The one-off execution problem. Some binaries are needed only once—a scaffolding tool, a code generator, a converter. Installing them as a project dependency pollutes the manifest and the lockfile. pnpm dlx installs the package in a temporary environment, runs the binary, and discards it. The package is never recorded in package.json or pnpm-lock.yaml.
The trade-off. Scripts are fast because they resolve locally. dlx is slower because it installs a package in a temporary directory. The resolution order is rigid, which means a script that shadows a binary name cannot be bypassed without the exec command. These are deliberate constraints, not limitations.
a. Resolving scripts and binaries with pnpm run
pnpm run <name> strictly resolves the name against the scripts field of the current workspace’s package.json. If no matching script exists, it fails with an error. To run a binary from a dependency, use pnpm exec <bin>.
# Explicitly run a script
pnpm run build
# Shorthand (pnpm checks scripts first, then binaries)
pnpm build
# Explicitly run a binary from node_modules/.bin
pnpm exec jest
The script runs from the directory of the workspace that declares it. If the workspace is a sub-package in a monorepo, the current working directory during execution is that sub-package’s directory, not the repository root.
The --recursive (-r) flag runs the script in every workspace that has it defined. The --filter (-F) flag limits execution to specified workspaces.
# Run build in every workspace that defines it
pnpm -r run build
# Run test only in the "utils" workspace
pnpm --filter utils run test
b. Passing arguments and forwarding flags
Arguments after the script name are passed to the script. For a script that invokes Jest, pnpm test -o --watch runs jest -o --watch. In pnpm, the -- separator is not required for argument forwarding; arguments after the script name are passed through.
# Arguments are forwarded to the script
pnpm test --coverage --watchAll=false
Node.js debugging flags are forwarded using the --node-options flag. The --inspect flag activates the inspector, and --inspect-brk pauses execution until a debugger attaches.
# Forward --inspect-brk to the Node process running webpack
pnpm run --node-options="--inspect-brk" webpack
c. One-off execution with pnpm dlx
pnpm dlx installs a package in a temporary project and runs a binary from that installation. The binary runs from the current working directory, so it can read project files, but the package itself is not added to the project’s dependencies.
# Run create-vite without installing it
pnpm dlx create-vite
# Specify a package if the binary name differs
pnpm dlx --package=typescript tsc --version
Using pnpm dlx as a replacement for pnpm add is explicitly discouraged. The packages installed by dlx are not tracked in the manifest or the lockfile, making the project non-deterministic. dlx is for one-off commands, not for dependencies the project relies on.
Complete Example Session
# ============================================
# PART 1: DEFINING SCRIPTS IN PACKAGE.JSON
# ============================================
{
"scripts": {
"build": "tsc",
"test": "jest",
"lint": "eslint src/"
}
}
# ============================================
# PART 2: RUNNING A SCRIPT
# ============================================
pnpm build
# Executes "tsc" from the workspace directory.
# ============================================
# PART 3: OMITTING THE RUN KEYWORD
# ============================================
pnpm test
# Identical to: pnpm run test
# ============================================
# PART 4: PASSING ARGUMENTS
# ============================================
pnpm test --coverage
# Executes: jest --coverage
# ============================================
# PART 5: RUNNING A DEPENDENCY BINARY
# ============================================
# If "tsc" is not a script but is a binary from TypeScript:
pnpm exec tsc --version
# Resolves to node_modules/.bin/tsc
# ============================================
# PART 6: RECURSIVE EXECUTION
# ============================================
pnpm -r run build
# Runs "build" in every workspace that defines it.
# ============================================
# PART 7: FILTERED EXECUTION
# ============================================
pnpm --filter utils run test
# Runs "test" only in the utils workspace.
# ============================================
# PART 8: NODE.JS FLAG FORWARDING
# ============================================
pnpm run --node-options="--inspect-brk" webpack
# Node.js runs webpack with --inspect-brk.
# ============================================
# PART 9: ONE-OFF EXECUTION WITH DLX
# ============================================
pnpm dlx create-vite my-app
# The package is installed temporarily and discarded.
The nine parts trace the execution model: defining scripts, running them with and without the run keyword, passing arguments, resolving binaries, recursive and filtered execution, forwarding Node.js flags, and executing one-off tools with dlx.
Quick Reference
Resolution Order
| Command | Resolution |
|---|---|
pnpm run <name> | scripts only |
pnpm <name> | scripts, then binaries |
pnpm exec <bin> | Binaries only |
pnpm run Flags
| Flag | Purpose |
|---|---|
-r, --recursive | Run in all workspaces |
-F, --filter <pattern> | Run in specified workspaces |
--node-options="..." | Forward flags to Node.js |
pnpm dlx Flags
| Flag | Purpose |
|---|---|
--package=<pkg> | Specify package when binary name differs |
Best Practices
✅ Do This:
pnpm run build
<!-- Use explicit run for scripts --> # ✅
pnpm test --coverage
<!-- Arguments after script name are forwarded --> # ✅
pnpm dlx create-vite
<!-- Use dlx for one-off scaffolding tools --> # ✅
❌ Don’t Do This:
pnpm add create-vite
<!-- One-off tools belong in dlx, not dependencies --> # ❌
# Rely on pnpm dlx for project dependencies
<!-- dlx packages are not tracked in the manifest --> # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Wrong tool executed | A script shadows a binary name | Use pnpm exec <bin> |
| Script runs from wrong directory | Monorepo workspace context | Use --filter or run from workspace |
| Arguments not forwarded | Shell interprets flags before pnpm | Quote flags if they contain special characters |
| dlx package not found | --package flag not used for differently-named binary | Specify the package with --package |
Real-World Examples
1. Run a Script
pnpm run build
2. Run with Arguments
pnpm test --watch
3. Execute a Binary
pnpm exec jest
4. Recursive Execution
pnpm -r run lint
5. Filtered Execution
pnpm --filter utils run test
6. Forward Node.js Flags
pnpm run --node-options="--inspect" dev
7. One-Off Scaffold
pnpm dlx create-vite my-app
8. Dlx with Custom Package
pnpm dlx --package=typescript tsc --version
9. List Scripts
pnpm run
# Lists all scripts in the current workspace.
10. List Binaries
ls node_modules/.bin/
Visual
Resolution Order
┌──────────────────────────────────────────────┐
│ pnpm run <name> │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Is <name> in package.json "scripts"? │
│ ├─ YES → Execute the script │
│ └─ NO → Error │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ pnpm <name> │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 1. Is <name> in package.json "scripts"? │
│ ├─ YES → Execute the script │
│ └─ NO → Continue │
│ │
│ 2. Does a dependency expose a binary? │
│ ├─ YES → Execute the binary │
│ └─ NO → Error │
└──────────────────────────────────────────────┘
Dlx vs Add
┌──────────────────────────────────────────────┐
│ pnpm dlx create-vite │
│ │
│ Temporary project → install → run → discard │
│ Not recorded in package.json or lockfile │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ pnpm add create-vite │
│ │
│ Project dependencies → install → recorded │
│ Available for future commands │
└──────────────────────────────────────────────┘
dlx is for one-off tools. add is for ongoing dependencies.
Summary
| Item | Value |
|---|---|
| Script execution | pnpm run <script> or pnpm <script> |
| Binary execution | pnpm exec <bin> |
Resolution order (run) | Scripts only |
| Resolution order (bare) | Scripts → binaries |
| Recursive | -r or --recursive |
| Filter | -F or --filter |
| Node.js flag forwarding | --node-options |
| One-off execution | pnpm dlx <package> |
Key takeaways:
- The
runkeyword is optional for scripts.pnpm testandpnpm run testare identical. Barepnpm <name>checks scripts first, then binaries. pnpm execbypasses scripts. When a script shadows a binary name,pnpm exec <bin>executes the binary directly.- Scripts run from the workspace directory. In a monorepo, a sub-package’s script runs from that sub-package’s directory, not from the root.
--node-optionsforwards flags to Node.js. The--inspectand--inspect-brkflags are applied to the Node process running the binary.pnpm dlxis for one-off tools. It installs a package temporarily, runs its binary, and discards it. The package is not recorded in the manifest or lockfile.
Remember: pnpm run is a strict resolver. It only looks at the scripts field. Bare pnpm <name> adds the binary fallback. pnpm exec skips the scripts entirely. And pnpm dlx is the escape hatch for tools that do not belong in the project’s dependency graph.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!