Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/pnpm-store/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,20 @@ manual `mounts` entry and no options required.
## How it works

1. **At build time** (`install.sh`): writes `store-dir=/workspaces/.pnpm-store`
into the remote user's `~/.npmrc` so pnpm uses the shared store globally.
into the remote user's `~/.npmrc`, and `storeDir: /workspaces/.pnpm-store`
into `~/.config/pnpm/config.yaml`, so pnpm uses the shared store globally
regardless of pnpm version (see note below).
2. **At container creation** (`postCreateCommand`): takes ownership of the
volume (named volumes start root-owned) and reports the effective pnpm
`store-dir`.
volume (named volumes start root-owned) and re-applies both config files,
then reports the effective pnpm `store-dir`.

> **pnpm 11 compatibility:** pnpm 11 stopped reading non-auth settings (like
> `store-dir`) from `.npmrc` — they must live in `pnpm-workspace.yaml` or the
> global `~/.config/pnpm/config.yaml`. If only `~/.npmrc` is set and the
> resolved pnpm is v11+, `store-dir` silently resolves to `undefined` and
> pnpm falls back to creating a `.pnpm-store` folder relative to the current
> working directory — exactly the stray folder this feature exists to avoid.
> This feature writes both files so it works across pnpm <11 and >=11.

## Ensuring pnpm is installed

Expand Down
2 changes: 1 addition & 1 deletion src/pnpm-store/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "pnpm-store",
"version": "1.0.6",
"version": "1.0.7",
"name": "pnpm Store (shared)",
"description": "Shares a single pnpm content-addressable store across every repo and across rebuilds via a Docker named volume, so no stray .pnpm-store folders pollute your repos. Zero-config and autonomous: the volume is created automatically, with no host directory to pre-create.",
"documentationURL": "https://github.com/helpers4/devcontainer/tree/main/src/pnpm-store",
Expand Down
44 changes: 43 additions & 1 deletion src/pnpm-store/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,31 @@ if [ "${USERNAME}" != "root" ]; then
fi
echo " ✅ Wrote store-dir to ${NPMRC}"

# 1b. pnpm 11 dropped support for non-auth settings (incl. store-dir) in
# .npmrc — they must live in ~/.config/pnpm/config.yaml instead. Write
# both so the feature works whether the resolved pnpm is <11 or >=11.
# Best-effort: a failure here must not abort the rest of the feature.
PNPM_CONFIG_DIR="${USER_HOME}/.config/pnpm"
PNPM_CONFIG_YAML="${PNPM_CONFIG_DIR}/config.yaml"
if mkdir -p "${PNPM_CONFIG_DIR}" 2>/dev/null; then
touch "${PNPM_CONFIG_YAML}"
# Strip any existing storeDir line so other keys (registry, hoist-pattern,
# etc.) already in config.yaml survive — same rationale as the .npmrc
# handling above. Write to .tmp first to keep the file atomic.
{ grep -v '^storeDir:' "${PNPM_CONFIG_YAML}" 2>/dev/null || true; } > "${PNPM_CONFIG_YAML}.tmp"
echo "storeDir: ${STORE_DIR}" >> "${PNPM_CONFIG_YAML}.tmp"
mv "${PNPM_CONFIG_YAML}.tmp" "${PNPM_CONFIG_YAML}"
if [ "${USERNAME}" != "root" ]; then
# Chown .config itself (non-recursive) so the user can create future
# subdirectories there, and only recurse into the pnpm subdir we own.
chown "${USERNAME}:${USER_GROUP}" "${USER_HOME}/.config" 2>/dev/null || true
chown -R "${USERNAME}:${USER_GROUP}" "${PNPM_CONFIG_DIR}" 2>/dev/null || true
fi
echo " ✅ Wrote storeDir to ${PNPM_CONFIG_YAML}"
else
echo " ⚠️ Could not create ${PNPM_CONFIG_DIR}; storeDir not written to config.yaml"
fi

# Create STORE_DIR during the image build so pnpm can use the configured path
# immediately. Without this, any pnpm invocation in a later feature (e.g.
# vite-plus) fails because /workspaces is not mounted at Docker build time and
Expand Down Expand Up @@ -173,6 +198,23 @@ echo "store-dir=${STORE_DIR}" >> "${NPMRC}.tmp"
mv "${NPMRC}.tmp" "${NPMRC}"
echo "✅ pnpm-store: store-dir=${STORE_DIR} written to ${NPMRC}"

# pnpm 11 dropped support for non-auth settings (incl. store-dir) in .npmrc —
# they must live in ~/.config/pnpm/config.yaml. Re-apply for the same reason
# as above (dotfiles-sync or a fresh install may not have it).
# Best-effort: a failure here must not abort the rest of the guard.
PNPM_CONFIG_DIR="${HOME}/.config/pnpm"
PNPM_CONFIG_YAML="${PNPM_CONFIG_DIR}/config.yaml"
if mkdir -p "${PNPM_CONFIG_DIR}" 2>/dev/null; then
# Strip any existing storeDir line so other keys already in config.yaml
# survive — same rationale as the .npmrc handling above.
{ grep -v '^storeDir:' "${PNPM_CONFIG_YAML}" 2>/dev/null || true; } > "${PNPM_CONFIG_YAML}.tmp"
echo "storeDir: ${STORE_DIR}" >> "${PNPM_CONFIG_YAML}.tmp"
mv "${PNPM_CONFIG_YAML}.tmp" "${PNPM_CONFIG_YAML}"
echo "✅ pnpm-store: storeDir=${STORE_DIR} written to ${PNPM_CONFIG_YAML}"
else
echo "⚠️ pnpm-store: could not create ${PNPM_CONFIG_DIR}; storeDir not written to config.yaml"
fi

# Confirm pnpm picked up the configured store, when available.
# pnpm config get returns the literal string "undefined" (exit 0) when the key
# is unset — treat it the same as empty.
Expand All @@ -186,7 +228,7 @@ if command -v pnpm >/dev/null 2>&1; then
echo "⚠️ pnpm-store: pnpm reports store-dir = ${configured} (expected ${STORE_DIR}); a local .npmrc may be overriding it"
fi
else
echo "ℹ️ pnpm-store: pnpm not on PATH yet; store-dir is set in ~/.npmrc for when it is."
echo "ℹ️ pnpm-store: pnpm not on PATH yet; store-dir is set in ~/.npmrc and ~/.config/pnpm/config.yaml for when it is."
fi
EOF

Expand Down
17 changes: 17 additions & 0 deletions test/pnpm-store/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@ else
exit 1
fi

# Test 2b: storeDir is configured in ~/.config/pnpm/config.yaml (pnpm 11+
# stopped reading store-dir from .npmrc).
found_config_yaml=""
for config_yaml in /root/.config/pnpm/config.yaml /home/*/.config/pnpm/config.yaml; do
[ -f "${config_yaml}" ] || continue
if grep -q "^storeDir: ${STORE_DIR}\$" "${config_yaml}"; then
found_config_yaml="${config_yaml}"
break
fi
done
if [ -n "${found_config_yaml}" ]; then
echo "✅ PASS: storeDir=${STORE_DIR} found in ${found_config_yaml}"
else
echo "❌ FAIL: storeDir=${STORE_DIR} not found in any config.yaml"
exit 1
fi

# Test 3: running the guard succeeds and the store directory exists.
# In real usage the named volume is mounted at STORE_DIR before the container
# starts, so the guard only needs to take ownership of it. The features-test
Expand Down