| | |

pnpm 07 📦 Running Scripts and Executing Binaries with pnpm run and pnpm dlx

⏱️ Estimated read: 8 min | |

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

CommandResolution
pnpm run <name>scripts only
pnpm <name>scripts, then binaries
pnpm exec <bin>Binaries only

pnpm run Flags

FlagPurpose
-r, --recursiveRun in all workspaces
-F, --filter <pattern>Run in specified workspaces
--node-options="..."Forward flags to Node.js

pnpm dlx Flags

FlagPurpose
--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

PitfallWhy It HappensFix
Wrong tool executedA script shadows a binary nameUse pnpm exec <bin>
Script runs from wrong directoryMonorepo workspace contextUse --filter or run from workspace
Arguments not forwardedShell interprets flags before pnpmQuote flags if they contain special characters
dlx package not found--package flag not used for differently-named binarySpecify 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

ItemValue
Script executionpnpm run <script> or pnpm <script>
Binary executionpnpm 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 executionpnpm dlx <package>

Key takeaways:

  • The run keyword is optional for scripts. pnpm test and pnpm run test are identical. Bare pnpm <name> checks scripts first, then binaries.
  • pnpm exec bypasses 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-options forwards flags to Node.js. The --inspect and --inspect-brk flags are applied to the Node process running the binary.
  • pnpm dlx is 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.

Was this helpful?


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!