From ac1ab0859d4437aa7d11921e0396f08e6cbe3c6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Caldero=CC=81n?= Date: Tue, 1 Sep 2026 14:27:01 +0200 Subject: [PATCH 1/4] Fix pnpm 11 audit commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔮 View transcript: https://nutrient-agentlogs.dev/s/q1jjdbu4fm9ooac2r8b7nc2g --- package.json | 18 +----------------- pnpm-workspace.yaml | 22 ++++++++++++++++++++++ scripts/audit-dependencies.sh | 8 ++++---- scripts/update-nutrient-in-examples.sh | 4 ++-- 4 files changed, 29 insertions(+), 23 deletions(-) create mode 100644 pnpm-workspace.yaml diff --git a/package.json b/package.json index 0a294196..dd7f4e5c 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "keywords": [], "author": "", "license": "MIT", + "packageManager": "pnpm@11.20.0", "devDependencies": { "@biomejs/biome": "^1.9.4", "@playwright/test": "^1.56.1", @@ -29,22 +30,5 @@ }, "lint-staged": { "*": ["biome check --write --no-errors-on-unmatched"] - }, - "pnpm": { - "overrides": { - "ajv": "^8.18.0", - "http-proxy-middleware": "^2.0.10", - "on-headers": "^1.1.0", - "node-forge": "^1.4.0", - "qs": "^6.15.1", - "shell-quote": "^1.9.0", - "fast-uri": "^3.1.4", - "picomatch": "^2.3.2", - "path-to-regexp": "^0.1.13", - "ws": "^8.21.0", - "websocket-driver": "^0.7.5", - "uuid": "^11.1.1" - }, - "onlyBuiltDependencies": ["@biomejs/biome"] } } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..1b3e881a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,22 @@ +packages: + - "." + +allowBuilds: + "@biomejs/biome": true + +onlyBuiltDependencies: + - "@biomejs/biome" + +overrides: + "ajv": "^8.18.0" + "http-proxy-middleware": "^2.0.10" + "on-headers": "^1.1.0" + "node-forge": "^1.4.0" + "qs": "^6.15.1" + "shell-quote": "^1.9.0" + "fast-uri": "^3.1.4" + "picomatch": "^2.3.2" + "path-to-regexp": "^0.1.13" + "ws": "^8.21.0" + "websocket-driver": "^0.7.5" + "uuid": "^11.1.1" diff --git a/scripts/audit-dependencies.sh b/scripts/audit-dependencies.sh index d9cd6db3..57294bec 100755 --- a/scripts/audit-dependencies.sh +++ b/scripts/audit-dependencies.sh @@ -34,10 +34,10 @@ for dir in examples/*; do if [ -f "pnpm-lock.yaml" ]; then has_lockfile=1 initial_json=$(pnpm audit --json 2>/dev/null) - # pnpm spells this `--fix` (there is no `pnpm audit fix` subcommand), - # and it only writes overrides into package.json — the install is what - # actually applies them to the lockfile and node_modules. - pnpm audit --fix > /dev/null 2>&1 + # pnpm 11 writes fixes to pnpm-workspace.yaml when using the override + # method. The install applies those overrides to the lockfile and + # node_modules. + pnpm audit --fix=override > /dev/null 2>&1 pnpm install --no-frozen-lockfile > /dev/null 2>&1 audit_json=$(pnpm audit --json 2>/dev/null) elif [ -f "package-lock.json" ]; then diff --git a/scripts/update-nutrient-in-examples.sh b/scripts/update-nutrient-in-examples.sh index a2c97906..9ad918ce 100755 --- a/scripts/update-nutrient-in-examples.sh +++ b/scripts/update-nutrient-in-examples.sh @@ -17,9 +17,9 @@ upgrade_npm_in_example() { if [ -f "pnpm-lock.yaml" ]; then pnpm install @nutrient-sdk/viewer@latest --save --save-exact - pnpm install > /dev/null + pnpm audit --fix=override > /dev/null || true - pnpm audit fix > /dev/null || true + pnpm install --no-frozen-lockfile > /dev/null elif [ -f "package-lock.json" ]; then npm install @nutrient-sdk/viewer@latest --save --save-exact From 97682725ede33a6667c456e36d3c1d2fe102d1f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Caldero=CC=81n?= Date: Wed, 2 Sep 2026 10:21:07 +0200 Subject: [PATCH 2/4] Finish the pnpm 11 migration and stop implicit installs - Export pnpm_config_minimum_release_age=0 in the bump and audit scripts: pnpm 11 defaults minimum-release-age to 24h and records an exclusion in the nearest pnpm-workspace.yaml for every fresher version it installs, which would rewrite the tracked example workspace files on every run. The env form also covers `pnpm audit --fix`, which ignores the --config flag. - Set verifyDepsBeforeRun: warn in the root workspace file and call tests/pnpm-workflows.test.sh directly in CI: pnpm 11 defaults the check to "install", so `pnpm run` (the pre-commit hook included) would install and possibly rewrite the tree before running anything. - Move the remaining root-level npm invocations (husky hook, e2e runner, README, PR template) to pnpm; export COREPACK_ENABLE_STRICT=0 from pnpm-helpers.sh and the Playwright web server for the npm calls that run inside the npm-based examples, and document it in AGENTS.md. - Name the failing example's reason in audit-dependencies.sh when the sibling workspace file is missing. - Extend the workflow test: structural check of the Setup pnpm step, npm/npx guard across all root-level files, and assertions for each of the above. Co-Authored-By: Claude Fable 5.1 --- .github/pull_request_template.md | 4 +-- .github/workflows/playwright.yml | 2 +- .husky/pre-commit | 4 +-- AGENTS.md | 6 ++++ README.md | 2 +- playwright.config.ts | 3 ++ pnpm-workspace.yaml | 5 +++ scripts/audit-dependencies.sh | 10 +++++- scripts/e2e-tests.sh | 2 +- scripts/pnpm-helpers.sh | 5 +++ scripts/update-nutrient-in-examples.sh | 8 +++++ tests/pnpm-workflows.test.sh | 50 ++++++++++++++++++++++---- 12 files changed, 86 insertions(+), 15 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4f649754..19647e9b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,8 +12,8 @@ ## Checklist -- [ ] `npm run format` passes (Biome) +- [ ] `pnpm run format` passes (Biome) - [ ] Example has `start` and `start:e2e` scripts in `package.json` -- [ ] `SERVER_DIR=examples/ npm run test` passes (Playwright smoke test) +- [ ] `SERVER_DIR=examples/ pnpm run test` passes (Playwright smoke test) - [ ] `README.md` included with prerequisites, setup, and usage - [ ] Uses current pinned `@nutrient-sdk/viewer` version diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 64413d1a..68cd00bd 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -22,7 +22,7 @@ jobs: with: node-version: lts/* - name: Check pnpm workflows - run: pnpm run test:pnpm-workflows + run: ./tests/pnpm-workflows.test.sh - name: Install dependencies run: pnpm install --frozen-lockfile && pnpm run install-dependencies - name: Install Playwright Browsers diff --git a/.husky/pre-commit b/.husky/pre-commit index 87be203e..6f97e948 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,2 @@ -npm run lint-staged -npm run check-biome-version +pnpm run lint-staged +pnpm run check-biome-version diff --git a/AGENTS.md b/AGENTS.md index 017fe945..e892a8ed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -91,6 +91,12 @@ pnpm run audit-fix pnpm run update-nutrient-version -- ``` +The root `packageManager` field pins pnpm, and Corepack applies that pin to every +directory below the root, including the npm-based examples. If you enabled +Corepack's npm shim (`corepack enable npm`), set `COREPACK_ENABLE_STRICT=0` before +running `npm` inside an example by hand; the scripts and the Playwright config set it +for you. Root-level scripts, hooks, and docs use pnpm. + ## Adding a New Example 1. Create `examples//` diff --git a/README.md b/README.md index 8f49890d..c8f726b7 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,6 @@ This repository includes a check to ensure that the Biome version in `package.js - During CI/CD in the GitHub workflow - As a pre-commit hook to prevent commits with mismatched versions -- Manually using `npm run check-biome-version` +- Manually using `pnpm run check-biome-version` If you update the Biome version in `package.json`, make sure to update it in `.github/workflows/biome.yml` as well. diff --git a/playwright.config.ts b/playwright.config.ts index 7a3bbed0..7d15364b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -33,6 +33,9 @@ export default defineConfig({ ], webServer: { command: `cd ${process.env.SERVER_DIR} && npm run start:e2e`, + // The examples are npm projects, but Corepack applies the root pnpm pin to + // them and rejects npm when its strict npm shim is enabled. + env: { COREPACK_ENABLE_STRICT: "0" }, url: "http://localhost:3000", reuseExistingServer: true, }, diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1b3e881a..f412f38a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,11 @@ packages: - "." +# pnpm 11 defaults verify-deps-before-run to "install", which would run a +# networked install from `pnpm run` (the pre-commit hook included) whenever +# node_modules is out of date. Warn instead so scripts never mutate the tree. +verifyDepsBeforeRun: warn + allowBuilds: "@biomejs/biome": true diff --git a/scripts/audit-dependencies.sh b/scripts/audit-dependencies.sh index 8881f028..a2a2e5c2 100755 --- a/scripts/audit-dependencies.sh +++ b/scripts/audit-dependencies.sh @@ -5,6 +5,12 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # shellcheck source=./pnpm-helpers.sh source "${SCRIPT_DIR}/pnpm-helpers.sh" +# pnpm 11 applies a 24-hour minimum-release-age by default and records an +# exclusion in the nearest pnpm-workspace.yaml for every fresh patch it lets +# through. The fix below exists to install those patches, so opt out instead of +# accumulating exclusions in the tracked example workspace files. +export pnpm_config_minimum_release_age=0 + echo -e "\033[37;1mAuditing npm vulnerabilities in examples\033[0m\r" vuln_dirs=() @@ -31,6 +37,7 @@ for dir in examples/*; do result=0 initialresult=0 audit_error=0 + audit_error_reason="pnpm/npm command failed or returned invalid data" has_lockfile=0 initial_json="" audit_json="" @@ -39,6 +46,7 @@ for dir in examples/*; do has_lockfile=1 if ! require_local_pnpm_workspace "$dir"; then audit_error=1 + audit_error_reason="no sibling pnpm-workspace.yaml" else initial_json=$(pnpm audit --json 2>/dev/null) # pnpm writes override fixes to the local workspace file. The @@ -69,7 +77,7 @@ for dir in examples/*; do fi if (( audit_error )); then - echo -e " \033[91mAudit could not complete\033[0m (pnpm/npm command failed or returned invalid data)" + echo -e " \033[91mAudit could not complete\033[0m (${audit_error_reason})" error_dirs+=("$dir") elif (( initialresult > 0 )); then ((fixed = initialresult - result)) diff --git a/scripts/e2e-tests.sh b/scripts/e2e-tests.sh index 3f1e8bbb..44a35ae0 100755 --- a/scripts/e2e-tests.sh +++ b/scripts/e2e-tests.sh @@ -16,7 +16,7 @@ for dir in examples/*; do done if [ "$skip" = false ]; then echo "Running e2e tests in $dir" - if ! (SERVER_DIR="$dir" npm run test); then + if ! (SERVER_DIR="$dir" pnpm run test); then failed+=("$dir") fi fi diff --git a/scripts/pnpm-helpers.sh b/scripts/pnpm-helpers.sh index f6faebcc..1a904644 100644 --- a/scripts/pnpm-helpers.sh +++ b/scripts/pnpm-helpers.sh @@ -1,5 +1,10 @@ #!/bin/bash +# These helpers back scripts that run npm inside the npm-based examples below +# the root pnpm pin. Corepack applies that pin to every directory under the +# root and, with its strict npm shim enabled, would reject those npm calls. +export COREPACK_ENABLE_STRICT=0 + require_local_pnpm_workspace() { local example="${1:-The current directory}" diff --git a/scripts/update-nutrient-in-examples.sh b/scripts/update-nutrient-in-examples.sh index 7b1b9ce0..6285ea4e 100755 --- a/scripts/update-nutrient-in-examples.sh +++ b/scripts/update-nutrient-in-examples.sh @@ -5,6 +5,14 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # shellcheck source=./pnpm-helpers.sh source "${SCRIPT_DIR}/pnpm-helpers.sh" +# pnpm 11 applies a 24-hour minimum-release-age by default and, for an exact +# version younger than that, records an exclusion in the nearest +# pnpm-workspace.yaml. This script exists to install a release that is hours +# old, so opt out here instead of letting every bump rewrite the tracked example +# workspace files. The env form also covers `pnpm audit --fix`, which ignores +# the --config flag. +export pnpm_config_minimum_release_age=0 + VERSION="${1:-}" if [ -z "${VERSION}" ]; then diff --git a/tests/pnpm-workflows.test.sh b/tests/pnpm-workflows.test.sh index cb70070a..d001c83b 100755 --- a/tests/pnpm-workflows.test.sh +++ b/tests/pnpm-workflows.test.sh @@ -9,6 +9,18 @@ fail() { exit 1 } +# Prints the body of the "Setup pnpm" step: everything after its name line up to +# the next step, so a `version:` input is caught wherever it sits in the step and +# a neighbouring step's `node-version:` never is. +setup_pnpm_step() { + awk '/name: Setup pnpm/ { inside = 1; next } inside && /^ *- / { exit } inside { print }' "$1" +} + +# Corepack applies the root packageManager pin to every directory below it and, +# with its strict npm shim enabled, rejects npm and npx. Root-level workflows, +# hooks, runners, and docs therefore use pnpm. +npm_invocation='(^|[^[:alnum:]])(npm (run|exec|install|ci)|npx)( |$)' + actual_package_manager="$(node -p "require('${REPO_ROOT}/package.json').packageManager")" [[ "$actual_package_manager" == "$EXPECTED_PACKAGE_MANAGER" ]] || \ fail "packageManager is not pinned to the expected integrity hash" @@ -17,7 +29,7 @@ for workflow in playwright.yml update-nutrient-sdk.yml; do workflow_path="${REPO_ROOT}/.github/workflows/${workflow}" grep -q 'name: Setup pnpm' "$workflow_path" || \ fail "${workflow} does not set up pnpm" - if grep -A3 'name: Setup pnpm' "$workflow_path" | grep -q 'version:'; then + if setup_pnpm_step "$workflow_path" | grep -Eq '^[[:space:]]*version:'; then fail "${workflow} overrides the packageManager pnpm version" fi done @@ -26,12 +38,36 @@ grep -q 'run: pnpm install --frozen-lockfile && pnpm run install-dependencies' \ "${REPO_ROOT}/.github/workflows/playwright.yml" || \ fail "Playwright CI does not use the pinned pnpm for the root install" -if grep -Eq 'run: npm (run|exec)' "${REPO_ROOT}/.github/workflows/playwright.yml"; then - fail "Playwright CI bypasses the pinned package manager" -fi -if grep -Eq '^npm (run|exec|install)' "${REPO_ROOT}/AGENTS.md"; then - fail "AGENTS.md documents npm commands that Corepack rejects in this pnpm project" -fi +for file in .github/workflows/playwright.yml .github/workflows/update-nutrient-sdk.yml \ + .github/pull_request_template.md AGENTS.md README.md .husky/pre-commit scripts/e2e-tests.sh; do + if grep -Eq "$npm_invocation" "${REPO_ROOT}/${file}"; then + fail "${file} invokes npm at the repository root, which Corepack rejects in this pnpm project" + fi +done + +# Under pnpm 11 `pnpm run` verifies dependencies first (installing them by +# default), so the workflow calls this script directly and the repository +# downgrades that check to a warning so scripts never mutate the tree. +grep -q 'run: ./tests/pnpm-workflows.test.sh' "${REPO_ROOT}/.github/workflows/playwright.yml" || \ + fail "Playwright CI must run this script directly instead of through pnpm run" +grep -q '^verifyDepsBeforeRun: warn$' "${REPO_ROOT}/pnpm-workspace.yaml" || \ + fail "pnpm-workspace.yaml must set verifyDepsBeforeRun: warn so pnpm run never installs implicitly" + +# The scripts and the e2e web server intentionally run npm inside the npm-based +# examples; Corepack would reject that under the root pin unless strict mode is off. +grep -q '^export COREPACK_ENABLE_STRICT=0$' "${REPO_ROOT}/scripts/pnpm-helpers.sh" || \ + fail "pnpm-helpers.sh must export COREPACK_ENABLE_STRICT=0 for the npm examples" +grep -q 'COREPACK_ENABLE_STRICT: "0"' "${REPO_ROOT}/playwright.config.ts" || \ + fail "playwright.config.ts must set COREPACK_ENABLE_STRICT=0 for the example web servers" + +# pnpm 11 defaults minimum-release-age to 24 hours and records an exclusion for +# every newer version in the nearest pnpm-workspace.yaml. The bump installs a +# version released hours earlier and the audit fix installs fresh patches, so +# without this opt-out both rewrite the tracked example workspace files. +for script in audit-dependencies.sh update-nutrient-in-examples.sh; do + grep -q '^export pnpm_config_minimum_release_age=0$' "${REPO_ROOT}/scripts/${script}" || \ + fail "${script} does not opt out of pnpm's minimum release age" +done for script in audit-dependencies.sh install-dependencies.sh update-nutrient-in-examples.sh; do script_path="${REPO_ROOT}/scripts/${script}" From c38b23d1bd028c812ecb348ae02ae7d66ebe1602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Caldero=CC=81n?= Date: Wed, 2 Sep 2026 15:03:51 +0200 Subject: [PATCH 3/4] Address pnpm workflow review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔮 View transcript: https://nutrient-agentlogs.dev/s/gghdh38myqd39wwo5ovmfy4v --- .github/workflows/update-nutrient-sdk.yml | 2 + AGENTS.md | 3 + scripts/audit-dependencies.sh | 17 ++--- scripts/pnpm-helpers.sh | 11 ++- scripts/update-nutrient-in-examples.sh | 10 +-- tests/pnpm-workflows.test.sh | 85 ++++++++++++++++++----- 6 files changed, 91 insertions(+), 37 deletions(-) diff --git a/.github/workflows/update-nutrient-sdk.yml b/.github/workflows/update-nutrient-sdk.yml index e9949a8f..1b26ca3b 100644 --- a/.github/workflows/update-nutrient-sdk.yml +++ b/.github/workflows/update-nutrient-sdk.yml @@ -156,6 +156,8 @@ jobs: pull request created by \`GITHUB_TOKEN\` does not trigger the Biome or Playwright workflows, so the checks tab here will be empty. + - Dependency audit fixes: pnpm examples may include security overrides + in \`pnpm-workspace.yaml\` and the corresponding lockfile updates - Formatting (\`pnpm run format\`): applied to \`examples/\` - E2E smoke tests (\`pnpm run e2e-tests\`): ${e2e_result} diff --git a/AGENTS.md b/AGENTS.md index e892a8ed..e16dda5f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,6 +71,9 @@ This updates `package.json` in every example directory. ## Commands ```bash +# One-time: enable the package-manager shims bundled with Node.js +corepack enable + # Install the root and all example dependencies with the pinned pnpm version pnpm install --frozen-lockfile pnpm run install-dependencies diff --git a/scripts/audit-dependencies.sh b/scripts/audit-dependencies.sh index a2a2e5c2..0cf80fd7 100755 --- a/scripts/audit-dependencies.sh +++ b/scripts/audit-dependencies.sh @@ -1,16 +1,12 @@ #!/bin/bash -# Runs npm/pnpm audit fix on all examples and outputs the result +# Runs npm/pnpm audit fix on all examples and outputs the result. +# This aggregate report deliberately does not enable errexit: predicates and +# arithmetic expressions below use nonzero statuses as ordinary control flow. SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # shellcheck source=./pnpm-helpers.sh source "${SCRIPT_DIR}/pnpm-helpers.sh" -# pnpm 11 applies a 24-hour minimum-release-age by default and records an -# exclusion in the nearest pnpm-workspace.yaml for every fresh patch it lets -# through. The fix below exists to install those patches, so opt out instead of -# accumulating exclusions in the tracked example workspace files. -export pnpm_config_minimum_release_age=0 - echo -e "\033[37;1mAuditing npm vulnerabilities in examples\033[0m\r" vuln_dirs=() @@ -52,10 +48,11 @@ for dir in examples/*; do # pnpm writes override fixes to the local workspace file. The # install applies them to the lockfile and node_modules. Real # command failures remain visible and make the audit fail. - if ! run_pnpm_audit_fix > /dev/null; then + if ! run_pnpm_audit_fix; then audit_error=1 elif ! run_pnpm_install_quietly --no-frozen-lockfile; then audit_error=1 + audit_error_reason="pnpm install failed; inspect ${dir}/pnpm-workspace.yaml for partial allowBuilds changes" else audit_json=$(pnpm audit --json 2>/dev/null) fi @@ -126,9 +123,7 @@ echo -e "\033[37;1m──────────────────── if [ ${#vuln_dirs[@]} -eq 0 ] && [ ${#error_dirs[@]} -eq 0 ]; then echo -e "\033[92;1mAll examples are vulnerability-free!\033[0m" -elif [ ${#vuln_dirs[@]} -eq 0 ]; then - echo -e "\033[92mNo remaining vulnerabilities found.\033[0m" -else +elif [ ${#vuln_dirs[@]} -gt 0 ]; then total_remaining=0 for i in "${!vuln_dirs[@]}"; do echo -e " \033[31m${vuln_dirs[$i]}: ${vuln_counts[$i]} vulnerabilities\033[0m (${vuln_severities[$i]})" diff --git a/scripts/pnpm-helpers.sh b/scripts/pnpm-helpers.sh index 1a904644..e2baade3 100644 --- a/scripts/pnpm-helpers.sh +++ b/scripts/pnpm-helpers.sh @@ -5,6 +5,12 @@ # root and, with its strict npm shim enabled, would reject those npm calls. export COREPACK_ENABLE_STRICT=0 +# pnpm 11 applies its minimum release age to every lockfile entry during +# install, not only while resolving new versions. All scripts must use the same +# policy so a lockfile written by the audit or update script remains installable +# in CI. The opt-out also avoids tracked minimumReleaseAgeExclude entries. +export pnpm_config_minimum_release_age=0 + require_local_pnpm_workspace() { local example="${1:-The current directory}" @@ -23,13 +29,16 @@ run_pnpm_command_quietly() { printf '%s\n' "$output" >&2 return "$status" fi + if [ -n "$output" ]; then + printf '%s\n' "$output" + fi } run_pnpm_audit_fix() { # A successful fix exits zero even when the initial audit found advisories. # Preserve every real pnpm failure instead of hiding bad flags, registry # failures, or an unwritable workspace file. - run_pnpm_command_quietly audit --fix=override + run_pnpm_command_quietly audit --fix=override --ignore-registry-errors } run_pnpm_install_quietly() { diff --git a/scripts/update-nutrient-in-examples.sh b/scripts/update-nutrient-in-examples.sh index 6285ea4e..8c17390e 100755 --- a/scripts/update-nutrient-in-examples.sh +++ b/scripts/update-nutrient-in-examples.sh @@ -5,14 +5,6 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # shellcheck source=./pnpm-helpers.sh source "${SCRIPT_DIR}/pnpm-helpers.sh" -# pnpm 11 applies a 24-hour minimum-release-age by default and, for an exact -# version younger than that, records an exclusion in the nearest -# pnpm-workspace.yaml. This script exists to install a release that is hours -# old, so opt out here instead of letting every bump rewrite the tracked example -# workspace files. The env form also covers `pnpm audit --fix`, which ignores -# the --config flag. -export pnpm_config_minimum_release_age=0 - VERSION="${1:-}" if [ -z "${VERSION}" ]; then @@ -40,7 +32,7 @@ upgrade_npm_in_example() { require_local_pnpm_workspace "examples/${directory}" pnpm install "@nutrient-sdk/viewer@${VERSION}" --save --save-exact - run_pnpm_audit_fix > /dev/null + run_pnpm_audit_fix # pnpm 11 treats an unapproved dependency build as an error. Keep this # fail-fast so the automated bump never continues with only some examples diff --git a/tests/pnpm-workflows.test.sh b/tests/pnpm-workflows.test.sh index d001c83b..77b27f61 100755 --- a/tests/pnpm-workflows.test.sh +++ b/tests/pnpm-workflows.test.sh @@ -17,8 +17,9 @@ setup_pnpm_step() { } # Corepack applies the root packageManager pin to every directory below it and, -# with its strict npm shim enabled, rejects npm and npx. Root-level workflows, -# hooks, runners, and docs therefore use pnpm. +# with its strict npm shim enabled, rejects npm package-management commands. +# Bare npx remains transparent to Corepack, so ban it as unpinned execution too. +# Root-level workflows, hooks, runners, and docs therefore use pnpm. npm_invocation='(^|[^[:alnum:]])(npm (run|exec|install|ci)|npx)( |$)' actual_package_manager="$(node -p "require('${REPO_ROOT}/package.json').packageManager")" @@ -40,8 +41,10 @@ grep -q 'run: pnpm install --frozen-lockfile && pnpm run install-dependencies' \ for file in .github/workflows/playwright.yml .github/workflows/update-nutrient-sdk.yml \ .github/pull_request_template.md AGENTS.md README.md .husky/pre-commit scripts/e2e-tests.sh; do + [ -f "${REPO_ROOT}/${file}" ] || \ + fail "${file} is missing; the npm guard no longer covers it" if grep -Eq "$npm_invocation" "${REPO_ROOT}/${file}"; then - fail "${file} invokes npm at the repository root, which Corepack rejects in this pnpm project" + fail "${file} invokes npm or unpinned npx at the repository root" fi done @@ -60,15 +63,6 @@ grep -q '^export COREPACK_ENABLE_STRICT=0$' "${REPO_ROOT}/scripts/pnpm-helpers.s grep -q 'COREPACK_ENABLE_STRICT: "0"' "${REPO_ROOT}/playwright.config.ts" || \ fail "playwright.config.ts must set COREPACK_ENABLE_STRICT=0 for the example web servers" -# pnpm 11 defaults minimum-release-age to 24 hours and records an exclusion for -# every newer version in the nearest pnpm-workspace.yaml. The bump installs a -# version released hours earlier and the audit fix installs fresh patches, so -# without this opt-out both rewrite the tracked example workspace files. -for script in audit-dependencies.sh update-nutrient-in-examples.sh; do - grep -q '^export pnpm_config_minimum_release_age=0$' "${REPO_ROOT}/scripts/${script}" || \ - fail "${script} does not opt out of pnpm's minimum release age" -done - for script in audit-dependencies.sh install-dependencies.sh update-nutrient-in-examples.sh; do script_path="${REPO_ROOT}/scripts/${script}" grep -q 'pnpm-helpers.sh' "$script_path" || \ @@ -80,6 +74,11 @@ done # shellcheck source=../scripts/pnpm-helpers.sh source "${REPO_ROOT}/scripts/pnpm-helpers.sh" +# pnpm 11 checks every lockfile entry against minimum-release-age during install. +# The shared opt-out keeps all three callers on one effective policy. +[[ "${pnpm_config_minimum_release_age:-}" == "0" ]] || \ + fail "pnpm callers do not effectively opt out of the minimum release age" + fixture="$(mktemp -d)" trap 'rm -rf "$fixture"' EXIT cd "$fixture" @@ -98,7 +97,24 @@ mkdir bin cat > bin/pnpm <<'PNPM' #!/bin/bash printf '%s\n' "$*" > "${PNPM_ARGS_FILE}" -echo "simulated audit failure" >&2 + +if [ "${PNPM_MODE:-fail}" = "fail-install" ]; then + if [ "${1:-}" = "audit" ] && [ "${2:-}" = "--json" ]; then + printf '%s\n' '{"metadata":{"vulnerabilities":{"total":0}}}' + exit 0 + fi + if [ "${1:-}" = "audit" ]; then + echo "3 overrides were added to pnpm-workspace.yaml" + exit 0 + fi +fi + +if [ -n "${PNPM_SUCCESS_OUTPUT:-}" ]; then + printf '%s\n' "$PNPM_SUCCESS_OUTPUT" + exit 0 +fi + +echo "simulated pnpm failure" >&2 exit 42 PNPM chmod +x bin/pnpm @@ -111,9 +127,9 @@ status=$? set -e [[ "$status" -eq 42 ]] || fail "pnpm audit failure status was swallowed" -[[ "$(cat pnpm-args.txt)" == 'audit --fix=override' ]] || \ +[[ "$(cat pnpm-args.txt)" == 'audit --fix=override --ignore-registry-errors' ]] || \ fail "pnpm audit fix used unexpected arguments" -grep -q 'simulated audit failure' audit-stderr.txt || \ +grep -q 'simulated pnpm failure' audit-stderr.txt || \ fail "pnpm audit failure details were hidden" set +e @@ -124,7 +140,44 @@ set -e [[ "$status" -eq 42 ]] || fail "pnpm install failure status was swallowed" [[ "$(cat pnpm-args.txt)" == 'install --no-frozen-lockfile' ]] || \ fail "pnpm install used unexpected arguments" -grep -q 'simulated audit failure' install-stderr.txt || \ +grep -q 'simulated pnpm failure' install-stderr.txt || \ fail "pnpm install failure details were hidden" +PNPM_SUCCESS_OUTPUT="3 overrides were added" run_pnpm_audit_fix >audit-success.txt +[[ "$(cat audit-success.txt)" == '3 overrides were added' ]] || \ + fail "successful pnpm mutation output was hidden" + +install_case="${fixture}/install-case" +mkdir -p "${install_case}/scripts" "${install_case}/examples/example" +cp "${REPO_ROOT}/scripts/install-dependencies.sh" \ + "${REPO_ROOT}/scripts/pnpm-helpers.sh" "${install_case}/scripts/" +touch "${install_case}/examples/example/pnpm-lock.yaml" \ + "${install_case}/examples/example/pnpm-workspace.yaml" +set +e +(cd "$install_case" && scripts/install-dependencies.sh) >install-script-stdout.txt 2>install-script-stderr.txt +status=$? +set -e +[[ "$status" -eq 42 ]] || \ + fail "install-dependencies.sh did not fail fast when pnpm install failed" + +audit_case="${fixture}/audit-case" +mkdir -p "${audit_case}/scripts" "${audit_case}/examples/example" +cp "${REPO_ROOT}/scripts/audit-dependencies.sh" \ + "${REPO_ROOT}/scripts/pnpm-helpers.sh" "${audit_case}/scripts/" +touch "${audit_case}/examples/example/pnpm-lock.yaml" \ + "${audit_case}/examples/example/pnpm-workspace.yaml" +set +e +PNPM_MODE=fail-install \ + bash -c 'cd "$1" && scripts/audit-dependencies.sh' _ "$audit_case" \ + >audit-script-stdout.txt 2>audit-script-stderr.txt +status=$? +set -e +[[ "$status" -eq 1 ]] || \ + fail "audit-dependencies.sh did not fail when an example could not be audited" +grep -q 'examples/example/pnpm-workspace.yaml' audit-script-stdout.txt || \ + fail "audit failure did not name the workspace file that may have changed" +if grep -q 'No remaining vulnerabilities found' audit-script-stdout.txt; then + fail "audit failure printed a contradictory success summary" +fi + echo "pnpm workflow checks passed" From 58d7ca5e64cd30393f0021200391159f7caaa1cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Caldero=CC=81n?= Date: Wed, 2 Sep 2026 16:15:02 +0200 Subject: [PATCH 4/4] Apply release age policy to pnpm examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔮 View transcript: https://nutrient-agentlogs.dev/s/gghdh38myqd39wwo5ovmfy4v --- examples/svelte-kit/pnpm-workspace.yaml | 2 ++ examples/vue-composition-api/pnpm-workspace.yaml | 2 ++ tests/pnpm-workflows.test.sh | 8 ++++++++ 3 files changed, 12 insertions(+) diff --git a/examples/svelte-kit/pnpm-workspace.yaml b/examples/svelte-kit/pnpm-workspace.yaml index 8f277024..e1bc04d5 100644 --- a/examples/svelte-kit/pnpm-workspace.yaml +++ b/examples/svelte-kit/pnpm-workspace.yaml @@ -1,3 +1,5 @@ +minimumReleaseAge: 0 + allowBuilds: esbuild: true diff --git a/examples/vue-composition-api/pnpm-workspace.yaml b/examples/vue-composition-api/pnpm-workspace.yaml index a373992b..b229c20d 100644 --- a/examples/vue-composition-api/pnpm-workspace.yaml +++ b/examples/vue-composition-api/pnpm-workspace.yaml @@ -1,3 +1,5 @@ +minimumReleaseAge: 0 + allowBuilds: esbuild: true diff --git a/tests/pnpm-workflows.test.sh b/tests/pnpm-workflows.test.sh index 77b27f61..8da738b9 100755 --- a/tests/pnpm-workflows.test.sh +++ b/tests/pnpm-workflows.test.sh @@ -79,6 +79,14 @@ source "${REPO_ROOT}/scripts/pnpm-helpers.sh" [[ "${pnpm_config_minimum_release_age:-}" == "0" ]] || \ fail "pnpm callers do not effectively opt out of the minimum release age" +for lockfile in "${REPO_ROOT}"/examples/*/pnpm-lock.yaml; do + workspace="$(dirname "$lockfile")/pnpm-workspace.yaml" + [ -f "$workspace" ] || \ + fail "${workspace#"${REPO_ROOT}/"} is missing" + grep -q '^minimumReleaseAge: 0$' "$workspace" || \ + fail "${workspace#"${REPO_ROOT}/"} does not disable the minimum release age for direct installs" +done + fixture="$(mktemp -d)" trap 'rm -rf "$fixture"' EXIT cd "$fixture"