Make Recursive pnpm Scripts Safe Across Workspace Working Directories
Recursive scripts can execute from each package directory, which changes relative path meaning. Use package-local paths or an explicit repository root for shared tasks.
Recursive execution changes the current directory
pnpm -r run build executes a script in workspace packages. A script that assumes it is running from the repository root may work when invoked manually and fail recursively because each process starts in a package directory. Relative config paths, shell globs, and output destinations are common sources of surprise.
Print the working directory in the failing script or run a harmless probe:
pnpm -r exec pwdDecide whether the task belongs to each package or to the workspace root. Package builds should usually use package-local configuration. A repository-wide generator should run as a root script and pass explicit paths to the packages it owns.
Avoid accidental cross-package writes
Do not make every package script write to ../../dist unless the workspace layout is guaranteed. Prefer package-local outputs or a task runner with declared inputs and outputs. This improves caching and prevents concurrent builds from overwriting the same location.
If a recursive command runs in parallel, check whether scripts share mutable state or depend on build order. Use pnpm's topological execution when dependencies must build first, and keep independent tests parallel. A task that succeeds only when run manually in a particular terminal is not a robust workspace script.
Add a clean-checkout CI job that runs the same root command developers use. That job reveals hidden local files, cached output, and directory assumptions. When the current directory is intentionally important, make it explicit in the script name and documentation rather than relying on a teammate to remember where they launched it.
Give each task a clear path contract
For every script, decide whether its paths are relative to the package, the workspace root, or an explicit argument. Package builds usually own package-local configuration and output. Repository-wide generators should run once from the root and receive explicit input directories. Mixing these contracts creates errors that appear only when pnpm runs a script recursively or in parallel.
Use a small diagnostic command to print the current directory and relevant resolved paths. Compare a direct invocation with a recursive invocation and check which script actually ran. Lifecycle scripts and nested pnpm run calls can change the effective working directory, so trace the process rather than assuming that the root shell's location is inherited by every child. Avoid “fixes” that add ../../ until they happen to work in today's directory structure.
Prevent parallel tasks from colliding
Recursive execution may run multiple packages concurrently. If every package writes to one shared dist folder or updates a common generated index, two tasks can overwrite one another even when each works alone. Give packages distinct output paths or serialize the shared task at the workspace root. Where a task runner supports declared inputs, outputs, and dependencies, keep those declarations aligned with what the script actually reads and writes.
Use topological execution when consumers depend on a workspace package's generated output. A package-level build may need to finish before an app's test starts. Independent lint or unit-test tasks can remain parallel. Make the dependency graph explicit rather than relying on accidental ordering observed on a developer laptop.
Finally run the same command from a clean checkout with no preexisting output. Assert that expected artifacts are created in the intended location and unrelated packages remain untouched. This catches hidden caches and path assumptions while keeping CI useful as a reproducibility check, not just a faster terminal.
Give every task a path contract: package-local, workspace-root, or explicitly passed. Recursive execution can run packages in parallel, so shared dist directories and generated indexes can collide even when each script works alone. Use distinct outputs or serialize the shared step, and use topological ordering when a consumer needs generated dependency output first. The clean-checkout assertion should verify both artifact location and absence of unexpected cross-package writes.
When a shared root task is intentional, make it a root script and pass package paths as arguments rather than relying on recursive working-directory behavior. When a package task is intentional, keep its configuration and output inside that package. This distinction makes caching, parallelism, and failure ownership clearer and prevents a command that works manually from becoming nondeterministic under pnpm -r.
Include one direct invocation and one recursive invocation in the script documentation when both are supported. Print the current directory only in diagnostics, not as a permanent noisy build step. A clear path contract lets contributors repair a failing script by changing the right boundary rather than adding more relative ../ segments.