Files
pnpm/exec/lifecycle
Eyal Mizrachi 2a39d62368 fix(lifecycle): mirror source into injected targets after prepare scripts
When `runLifecycleHooksConcurrently` re-imports a workspace package into
its injected targets after running prepare/postinstall, it has to
preserve the existing `node_modules/` inside each target — set up
during the initial install (bin symlinks, transitive deps) — while
overlaying the script's output (typically a freshly-built `dist/`).

The previous approach was a manual `scanDir` over `<targetDir>/node_modules`
that fed absolute paths into the re-import's `filesMap`. That worked in
2022 (#4299) when `importIndexedDir` always staged-and-renamed, but
#11088 made the fast path (write directly into the final dir) the
default. The fast path's `makeEmptyDirSync(targetDir)` wipes the very
files the scanned entries point at, so `tryImportIndexedDir`'s first
copy throws:

    ERR_PNPM_ENOENT on <targetDir>/node_modules/.bin/<tool>

Reproduces on any workspace package that has a `prepare`/`postinstall`
script + a dep with a bin entry, when injected via
`injectWorkspacePackages: true` or `dependenciesMeta.<dep>.injected`.

Fix: skip the `importPackage` round-trip entirely. After lifecycle
scripts complete, iterate the source's `filesMap` (from `fetchFromDir`,
which already excludes `node_modules/`) and copy each entry directly
into each `targetDir`. The target's existing `node_modules/` is never
touched — its bin links + transitive deps stay intact — and there's no
tmp-dir staging swap to trip over `.bin/<tool>` symlinks.

Regression test in `injectLocalPackages.ts`: an injected workspace
package with a `prepublishOnly` script + `@pnpm.e2e/hello-world-js-bin`
dep. Before the fix this throws `ERR_PNPM_ENOENT` on
`.bin/hello-world-js-bin`; after the fix both the script's output and
the bin symlink survive into the injected copy.
2026-05-15 03:19:37 -04:00
..
2026-05-14 13:31:53 +02:00
2026-05-14 13:31:53 +02:00

@pnpm/exec.lifecycle

Package lifecycle hook runner

npm version

Installation

pnpm add @pnpm/logger @pnpm/exec.lifecycle

Usage

import runLifecycleHook, {runPostinstallHooks} from '@pnpm/exec.lifecycle'

const targetPkgRoot = path.resolve('node_modules/target-pkg')
const pkg = require(path.join(targetPkgRoot, 'package.json'))

// Run a specific hook
await runLifecycleHook('preinstall', pkg, {
  pkgId: 'target-pkg/1.0.0',
  pkgRoot: targetPkgRoot,
  rawConfig: {},
  rootModulesDir: path.resolve('node_modules'),
  unsafePerm: true,
})

// Run all install hooks
await runPostinstallHooks({
  pkgId: 'target-pkg/1.0.0',
  pkgRoot: targetPkgRoot,
  rawConfig: {},
  rootModulesDir: path.resolve('node_modules'),
  unsafePerm: true,
})

API

runLifecycleHook(stage, packageManifest, opts): Promise<void>

runPostinstallHooks(opts): Promise<void>

License

MIT