fix(scripts): prefer Windows .cmd shims for pnpm (#5305)

* fix(scripts): prefer Windows cmd shims for pnpm

* fix(scripts): address pnpm Windows review feedback

* docs: reduce inherited agent guidance size
This commit is contained in:
dong 2026-09-11 18:41:24 +08:00 committed by GitHub
parent b809b7bc7d
commit f09e824460
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 49 additions and 14 deletions

View File

@ -166,7 +166,10 @@ cd frontend && pnpm test # Unit tests
Rule of thumb: **root `make` = the full application**; **`backend/Makefile` and `frontend/`
(`pnpm`) = per-module work.**
Host-side pnpm consumers, including the root/frontend Makefiles and local diagnostic scripts, must run through `scripts/pnpm.py`. Diagnostic scripts resolve the runner and frontend directory to absolute paths before changing the child process working directory, so they remain independent of the caller's current directory. The runner preserves direct `pnpm`/`pnpm.cmd` priority, falls back to `corepack pnpm`, and is invoked from `frontend/` so Corepack honors the package-manager version pinned by that project.
Host pnpm calls use `scripts/pnpm.py`: native Windows tries `pnpm.cmd` before
`pnpm`; POSIX reverses the order. Its Corepack fallback applies the same ordering
to `corepack.cmd` and `corepack`. The runner operates from `frontend/` so
Corepack honors its pinned package-manager version.
### Prerequisites before `make dev`

View File

@ -365,7 +365,7 @@ such a checkout, use `bash ./scripts/<name>.sh ...`.
make check # Verifies Node.js 22+, pnpm, uv, nginx
```
The local `make check`, `make install`, `make dev`, and `make start` entry points use a direct `pnpm`/`pnpm.cmd` executable when available and otherwise fall back to `corepack pnpm`. The shared runner and diagnostics resolve repository paths absolutely, so these checks work regardless of the caller's current directory. Corepack runs from `frontend/`, so it honors the `packageManager` version pinned in `frontend/package.json`; enabling a global pnpm shim is not required.
The local `make check`, `make install`, `make dev`, and `make start` entry points use a direct `pnpm` executable when available and otherwise fall back to `corepack pnpm`. With native Windows Python, the shared runner checks `pnpm.cmd` before the generic `pnpm` lookup, which follows `PATH`/`PATHEXT` and may select an `.exe` or `.bat` in the same or an earlier PATH directory. The Corepack fallback likewise checks `corepack.cmd` before `corepack`. POSIX Python keeps the generic names first, including when running under MSYS/Cygwin. The runner and diagnostics resolve repository paths absolutely, so these checks work regardless of the caller's current directory. Corepack runs from `frontend/`, so it honors the `packageManager` version pinned in `frontend/package.json`; enabling a global pnpm shim is not required.
2. **Install dependencies**:
```bash

View File

@ -2,16 +2,19 @@ from __future__ import annotations
import json
import os
import runpy
import shutil
import subprocess
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
PNPM_SCRIPT = REPO_ROOT / "scripts" / "pnpm.py"
FRONTEND_DIR = REPO_ROOT / "frontend"
PNPM_MODULE = runpy.run_path(str(PNPM_SCRIPT))
def _write_fake_command(bin_dir: Path, name: str, label: str, exit_code: int = 0) -> Path:
@ -62,6 +65,30 @@ def test_runner_prefers_direct_pnpm_and_forwards_arguments(tmp_path: Path):
assert "via Corepack" not in result.stderr
def test_runner_prefers_cmd_shim_on_windows(monkeypatch):
paths = {
"pnpm": r"C:\tools\pnpm.exe",
"pnpm.cmd": r"C:\tools\pnpm.cmd",
}
find_pnpm_command = PNPM_MODULE["find_pnpm_command"]
monkeypatch.setitem(find_pnpm_command.__globals__, "os", SimpleNamespace(name="nt"))
monkeypatch.setitem(find_pnpm_command.__globals__, "shutil", SimpleNamespace(which=paths.get))
assert find_pnpm_command() == [paths["pnpm.cmd"]]
def test_runner_prefers_corepack_cmd_shim_on_windows(monkeypatch):
paths = {
"corepack": r"C:\tools\corepack.exe",
"corepack.cmd": r"C:\tools\corepack.cmd",
}
find_pnpm_command = PNPM_MODULE["find_pnpm_command"]
monkeypatch.setitem(find_pnpm_command.__globals__, "os", SimpleNamespace(name="nt"))
monkeypatch.setitem(find_pnpm_command.__globals__, "shutil", SimpleNamespace(which=paths.get))
assert find_pnpm_command() == [paths["corepack.cmd"], "pnpm"]
def test_runner_uses_corepack_pnpm_from_frontend_directory(tmp_path: Path):
bin_dir = tmp_path / "bin"
bin_dir.mkdir()

View File

@ -15,6 +15,14 @@ likewise prefix the target with `bash`. This keeps documented `make` commands
working when a source archive, `core.fileMode=false`, or a non-POSIX filesystem
does not preserve executable bits.
Host-side pnpm calls must go through `scripts/pnpm.py`. With native Windows
Python (`os.name == "nt"`), it checks `pnpm.cmd` before the generic `pnpm`
lookup, which uses `PATH`/`PATHEXT` and may select an `.exe` or `.bat` in the
same or an earlier PATH directory. If neither is found, it falls back to
Corepack, checking `corepack.cmd` before `corepack`. POSIX Python (including
MSYS/Cygwin Python) keeps the generic name first for each tool; the gate is
based on Python's `os.name`, not the invoking shell.
## Public Skill Review Waivers
`review_changed_public_skills.py` keeps the analyzer strict and applies narrow

View File

@ -3,6 +3,7 @@
from __future__ import annotations
import os
import shutil
import subprocess
import sys
@ -15,19 +16,15 @@ COREPACK_NOTICE = "Using pnpm via Corepack."
def find_pnpm_command() -> list[str] | None:
"""Return the preferred pnpm-compatible command for this machine."""
pnpm_path = shutil.which("pnpm")
if pnpm_path:
return [str(Path(pnpm_path))]
pnpm_names = ("pnpm.cmd", "pnpm") if os.name == "nt" else ("pnpm", "pnpm.cmd")
for name in pnpm_names:
if pnpm_path := shutil.which(name):
return [str(Path(pnpm_path))]
pnpm_cmd_path = shutil.which("pnpm.cmd")
if pnpm_cmd_path:
return [str(Path(pnpm_cmd_path))]
corepack_path = shutil.which("corepack")
if not corepack_path:
corepack_path = shutil.which("corepack.cmd")
if corepack_path:
return [str(Path(corepack_path)), "pnpm"]
corepack_names = ("corepack.cmd", "corepack") if os.name == "nt" else ("corepack", "corepack.cmd")
for name in corepack_names:
if corepack_path := shutil.which(name):
return [str(Path(corepack_path)), "pnpm"]
return None