Skip to content

Commit 23e5fce

Browse files
authored
Fix pnpm interaction with custom cache (#1522)
* Fix pnpm interaction with custom cache When a `cacheDirectories` field is set in `package.json`, this triggers [custom cache behavior](https://devcenter.heroku.com/articles/nodejs-classic-buildpack-builds#custom-caching) in the buildpack. This is often used with tools that produce their own specialized caches like Next.js which can cache asset compilation results. Since custom cache behavior means the user is taking full control of what needs to be cached, the `node_modules` folder is typically added to the cache directory list to ensure that installed modules are also cached. For npm and Yarn this doesn't present a problem but, for pnpm, [the `node_modules` folder is used differently](https://pnpm.io/motivation#creating-a-non-flat-node_modules-directory) and, if the folder already exists (e.g.; on cache restore), it will cause the build to halt with an `ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY` error. After examining the custom cache code, I'm certain that even how npm and Yarn handle this scenario is incorrect. The user should never cache the `node_modules` folder. What should happen instead is (for default or custom cache behavior), the npm cache, Yarn cache, and pnpm store are what should be always cached unless `NODE_MODULES_CACHE=false`. For now, I'm just going to address this pnpm case though. This PR makes the following changes: ### Custom Cache Behavior with pnpm - if `node_modules` is specified, it will be ignored - all other cache directories will be processed - the pnpm store will be inserted into the custom cache list ### pnpm Reporter A `PNPM_INSTALL_REPORTER` environment variable was introduced to allow for more detailed pnpm output to be produced during install. If set it, the value of this environment variable will be used to append the [`--reporter`](https://pnpm.io/cli/install#--reportername) argument when `pnpm install` is executed. This is used by the tests in this PR to ensure that the pnpm store is functioning correctly, but it could also be useful when debugging builds where pnpm installs are failing unexpectedly. * Use explicit comparison of `PNPM` variable to `true` * Use a more precise pattern for matching the `node_modules` folder * Fix typo * Validate the PNPM_INSTALL_REPORTER env var * Update CHANGELOG.md Signed-off-by: Colin Casey <[email protected]> * Fix test output validations --------- Signed-off-by: Colin Casey <[email protected]>
1 parent 15a348b commit 23e5fce

File tree

8 files changed

+105
-12
lines changed

8 files changed

+105
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## [Unreleased]
44

5+
- Fix for pnpm + custom caching. ([#1522](https://github.com/heroku/heroku-buildpack-nodejs/pull/1522))
56

67
## [v322] - 2025-12-11
78

bin/compile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ restore_cache() {
339339
if [[ "$cache_directories" == "" ]]; then
340340
restore_default_cache_directories "$BUILD_DIR" "$CACHE_DIR" "$YARN_CACHE_FOLDER" "$NPM_CONFIG_CACHE" "$PNPM_CONFIG_CACHE"
341341
else
342-
restore_custom_cache_directories "$BUILD_DIR" "$CACHE_DIR" "$cache_directories"
342+
restore_custom_cache_directories "$BUILD_DIR" "$CACHE_DIR" "$PNPM_CONFIG_CACHE" "$cache_directories"
343343
fi
344344
elif [[ "$cache_status" == "new-signature" ]]; then
345345
header "Restoring cache"
@@ -414,7 +414,7 @@ cache_build() {
414414
save_default_cache_directories "$BUILD_DIR" "$CACHE_DIR" "$YARN_CACHE_FOLDER" "$NPM_CONFIG_CACHE" "$PNPM_CONFIG_CACHE"
415415
else
416416
header "Caching build"
417-
save_custom_cache_directories "$BUILD_DIR" "$CACHE_DIR" "$cache_directories"
417+
save_custom_cache_directories "$BUILD_DIR" "$CACHE_DIR" "$PNPM_CONFIG_CACHE" "$cache_directories"
418418
fi
419419
save_signature "$CACHE_DIR"
420420
build_data::set_duration "save_cache_time" "$cache_build_start_time"

lib/cache.sh

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,16 @@ restore_custom_cache_directories() {
108108
local cache_directories
109109
local build_dir=${1:-}
110110
local cache_dir=${2:-}
111+
local pnpm_cache_dir=${3:-}
111112
# Parse the input string with multiple lines: "a\nb\nc" into an array
112-
mapfile -t cache_directories <<< "$3"
113+
mapfile -t cache_directories <<< "$4"
113114

114-
echo "Loading ${#cache_directories[@]} from cacheDirectories (package.json):"
115+
echo "Loading from cacheDirectories (package.json):"
115116

116117
for cachepath in "${cache_directories[@]}"; do
117-
if [ -e "$build_dir/$cachepath" ]; then
118+
if [[ "$PNPM" == "true" ]] && [[ "$cachepath" =~ ^node_modules(/|$) ]]; then
119+
echo "- $cachepath (skipping because pnpm is used)"
120+
elif [ -e "$build_dir/$cachepath" ]; then
118121
echo "- $cachepath (exists - skipping)"
119122
else
120123
if [ -e "$cache_dir/node/cache/$cachepath" ]; then
@@ -126,6 +129,15 @@ restore_custom_cache_directories() {
126129
fi
127130
fi
128131
done
132+
133+
if [[ "$PNPM" == "true" ]] && [ -e "$cache_dir/node/cache/pnpm/store" ]; then
134+
echo "- pnpm store (included because pnpm is used)"
135+
# the $pnpm_cache_dir is created at the start of the build so, now, if we want to
136+
# rename the cache directory to $pnpm_cache_dir, we have to remove it or we'll
137+
# end up with a $pnpm_cache_dir/store directory instead of $pnpm_cache_dir.
138+
rm -rf "$pnpm_cache_dir"
139+
mv "$cache_dir/node/cache/pnpm/store" "$pnpm_cache_dir"
140+
fi
129141
}
130142

131143
clear_cache() {
@@ -192,13 +204,16 @@ save_custom_cache_directories() {
192204
local cache_directories
193205
local build_dir=${1:-}
194206
local cache_dir=${2:-}
207+
local pnpm_cache_dir=${3:-}
195208
# Parse the input string with multiple lines: "a\nb\nc" into an array
196-
mapfile -t cache_directories <<< "$3"
209+
mapfile -t cache_directories <<< "$4"
197210

198-
echo "Saving ${#cache_directories[@]} cacheDirectories (package.json):"
211+
echo "Saving cacheDirectories (package.json):"
199212

200213
for cachepath in "${cache_directories[@]}"; do
201-
if [ -e "$build_dir/$cachepath" ]; then
214+
if [[ "$PNPM" == "true" ]] && [[ "$cachepath" =~ ^node_modules(/|$) ]]; then
215+
echo "- $cachepath (skipping because pnpm is used)"
216+
elif [ -e "$build_dir/$cachepath" ]; then
202217
echo "- $cachepath"
203218
mkdir -p "$cache_dir/node/cache/$cachepath"
204219
cp -a "$build_dir/$cachepath" "$(dirname "$cache_dir/node/cache/$cachepath")"
@@ -207,6 +222,12 @@ save_custom_cache_directories() {
207222
fi
208223
done
209224

225+
if [[ "$PNPM" == "true" ]] && [ -e "$pnpm_cache_dir" ]; then
226+
echo "- pnpm store (included because pnpm is used)"
227+
mkdir -p "$cache_dir/node/cache/pnpm"
228+
cp -a "$pnpm_cache_dir" "$cache_dir/node/cache/pnpm/store"
229+
fi
230+
210231
build_data::set_raw "has_custom_cache_dirs" "true"
211232
}
212233

lib/dependencies.sh

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,21 @@ pnpm_install() {
305305
echo "Running 'pnpm install' with pnpm-lock.yaml"
306306
cd "$build_dir" || return
307307

308-
monitor "install_dependencies" pnpm install --prod=false --frozen-lockfile 2>&1
308+
pnpm_install_args=("install" "--prod=false" "--frozen-lockfile")
309+
310+
if [ -n "$PNPM_INSTALL_REPORTER" ]; then
311+
case "$PNPM_INSTALL_REPORTER" in
312+
default|ndjson|append-only|silent)
313+
pnpm_install_args+=("--reporter=$PNPM_INSTALL_REPORTER")
314+
;;
315+
*)
316+
echo "Warning: Invalid PNPM_INSTALL_REPORTER value '$PNPM_INSTALL_REPORTER'. Valid values: default, ndjson, append-only, silent"
317+
echo "Proceeding with default reporter"
318+
;;
319+
esac
320+
fi
321+
322+
monitor "install_dependencies" pnpm "${pnpm_install_args[@]}" 2>&1
309323

310324
# prune the store when the counter reaches zero to clean up errant package versions which may have been upgraded/removed
311325
counter=$(load_pnpm_prune_store_counter "$cache_dir")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Just a simple cached file :)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "pnpm-custom-cache",
3+
"packageManager": "[email protected]",
4+
"dependencies": {
5+
"dotenv": "^17.2.3"
6+
},
7+
"cacheDirectories": [
8+
"node_modules",
9+
"data"
10+
]
11+
}

test/fixtures/pnpm-custom-cache/pnpm-lock.yaml

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/run

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ testBuildWithUserCacheDirectoriesCamel() {
557557
assertCapturedSuccess
558558

559559
compile "cache-directories-camel" $cache
560-
assertCaptured "Loading 3 from cacheDirectories"
560+
assertCaptured "Loading from cacheDirectories"
561561
assertCaptured "- server/node_modules"
562562
assertCaptured "- client/node_modules"
563563
assertCaptured "- non/existent (not cached - skipping)"
@@ -963,13 +963,13 @@ testBuildWithUserCacheDirectories() {
963963
cache=$(mktmpdir)
964964

965965
compile "cache-directories" $cache
966-
assertCaptured "Saving 2 cacheDirectories"
966+
assertCaptured "Saving cacheDirectories"
967967
assertEquals "1" "$(ls -1 $cache/node/cache | grep -c bower_components | tr -d ' ')"
968968
assertEquals "1" "$(ls -1 $cache/node/cache | grep -c node_modules | tr -d ' ')"
969969
assertCapturedSuccess
970970

971971
compile "cache-directories" $cache
972-
assertCaptured "Loading 2 from cacheDirectories"
972+
assertCaptured "Loading from cacheDirectories"
973973
assertCaptured "- node_modules"
974974
assertCaptured "- bower_components"
975975
assertCapturedSuccess
@@ -1716,6 +1716,28 @@ testCorepackNpmRegistryKeyFix() {
17161716

17171717
# PNPM
17181718

1719+
testPnpmWithCustomCache() {
1720+
cache_dir=$(mktmpdir)
1721+
env_dir=$(mktmpdir)
1722+
echo "ndjson" > "$env_dir/PNPM_INSTALL_REPORTER"
1723+
1724+
compile "pnpm-custom-cache" "$cache_dir" "$env_dir"
1725+
assertCaptured "pnpm:fetching-progress"
1726+
assertCaptured "Saving cacheDirectories (package.json):"
1727+
assertCaptured "- node_modules (skipping because pnpm is used)"
1728+
assertCaptured "- data"
1729+
assertCaptured "- pnpm store (included because pnpm is used)"
1730+
assertCapturedSuccess
1731+
1732+
compile "pnpm-custom-cache" "$cache_dir" "$env_dir"
1733+
assertCaptured " Loading from cacheDirectories (package.json):"
1734+
assertCaptured "- node_modules (skipping because pnpm is used)"
1735+
assertCaptured "- data (exists - skipping)"
1736+
assertCaptured "- pnpm store (included because pnpm is used)"
1737+
assertNotCaptured "pnpm:fetching-progress"
1738+
assertCapturedSuccess
1739+
}
1740+
17191741
testPnpm7Pnp() {
17201742
build_dir=$(mktmpdir)
17211743
cache_dir=$(mktmpdir)

0 commit comments

Comments
 (0)