diff --git a/src/pnpm-store/README.md b/src/pnpm-store/README.md index 76a5996..126c85b 100644 --- a/src/pnpm-store/README.md +++ b/src/pnpm-store/README.md @@ -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 diff --git a/src/pnpm-store/devcontainer-feature.json b/src/pnpm-store/devcontainer-feature.json index 85399a2..0ef16c5 100644 --- a/src/pnpm-store/devcontainer-feature.json +++ b/src/pnpm-store/devcontainer-feature.json @@ -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", diff --git a/src/pnpm-store/install.sh b/src/pnpm-store/install.sh index 5efd98c..560ff3c 100644 --- a/src/pnpm-store/install.sh +++ b/src/pnpm-store/install.sh @@ -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 @@ -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. @@ -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 diff --git a/test/pnpm-store/test.sh b/test/pnpm-store/test.sh index 06e686e..a250040 100644 --- a/test/pnpm-store/test.sh +++ b/test/pnpm-store/test.sh @@ -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