From 030d6ef5f5268f933b69d55842b465a17ec96f2a Mon Sep 17 00:00:00 2001 From: Ritesh Kumar Date: Thu, 27 Aug 2026 13:24:29 +0530 Subject: [PATCH 1/3] Fix CODEOWNERS and make SDK version bumps explicit The CODEOWNERS file referenced @PSPDFKit/nickel, a team that does not exist, so no review was ever auto-requested. Point it at @PSPDFKit/web, which has write access to this repository. The CDN updater keyed its map on "gatsby" while the directory is "gatsbyjs", and omitted nuxtjs and the Salesforce README entirely. Those three files were left pinned at 1.8.0, 1.3.0 and 1.0.0 while every other example moved to 1.18.0. Correct the key, add the missing entries, and validate all paths before writing so a moved file fails the run instead of silently half-updating an example. Both scripts now take the target version as a required argument instead of resolving @latest at install time, so a release landing mid-run cannot produce a bump whose title and lockfiles disagree. --- .github/CODEOWNERS | 2 +- AGENTS.md | 4 +- scripts/update-nutrient-in-cdn.js | 56 +++++++++++++++++--------- scripts/update-nutrient-in-examples.sh | 20 +++++++-- 4 files changed, 57 insertions(+), 25 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6892fa40..5a33481b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # Web SDK team owns all examples -* @PSPDFKit/nickel +* @PSPDFKit/web diff --git a/AGENTS.md b/AGENTS.md index e9b1ed7a..efa0fa47 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,8 +85,8 @@ SERVER_DIR=examples/javascript-vite npm run test # Audit and fix vulnerabilities across all examples npm run audit-fix -# Bump Nutrient SDK version in all examples -npm run update-nutrient-version +# Bump Nutrient SDK version in all examples (version is required) +npm run update-nutrient-version -- 1.21.0 ``` ## Adding a New Example diff --git a/scripts/update-nutrient-in-cdn.js b/scripts/update-nutrient-in-cdn.js index 948bbed6..9a6b41f2 100644 --- a/scripts/update-nutrient-in-cdn.js +++ b/scripts/update-nutrient-in-cdn.js @@ -1,39 +1,59 @@ const fs = require("node:fs"); const path = require("node:path"); -const { execSync } = require("node:child_process"); const cdnOcurrences = { - typescript: ["src/index.html"], - gatsby: ["src/templates/Viewport.js"], - salesforce: ["force-app/main/default/pages/Nutrient_InitNutrient.page"], + gatsbyjs: ["src/templates/Viewport.js"], "javascript-vite": ["index.html"], + nuxtjs: ["components/NutrientContainer.vue"], + salesforce: [ + "README.md", + "force-app/main/default/pages/Nutrient_InitNutrient.page", + ], + typescript: ["src/index.html"], "typescript-vite": ["index.html"], webpack: ["README.md", "src/index.html"], }; const example = process.argv[2]; +const version = process.argv[3]; + +if (!example || !version) { + console.error( + "Usage: node scripts/update-nutrient-in-cdn.js ", + ); + process.exit(1); +} + +if (!/^\d+\.\d+\.\d+$/.test(version)) { + console.error(`Expected a semver version, got "${version}".`); + process.exit(1); +} if (cdnOcurrences[example]) { console.log(`Updating CDN version in ${example} example.`); - for (const relativePath of cdnOcurrences[example]) { - const template = fs.readFileSync( - path.resolve(`./examples/${example}/${relativePath}`), - "utf8", - ); + const filePaths = cdnOcurrences[example].map((relativePath) => + path.resolve(__dirname, "..", "examples", example, relativePath), + ); - const version = execSync("npm view @nutrient-sdk/viewer version") - .toString() - .trim(); + // Validate every file up front: a renamed or moved file would otherwise be + // skipped silently, or abort the run half-written across the example. + const missing = filePaths.filter((filePath) => !fs.existsSync(filePath)); - const updatedTemplate = template.replace( - /pspdfkit-web@([0-9]+.[0-9]+.[0-9]+)?/g, - `pspdfkit-web@${version}`, - ); + if (missing.length > 0) { + console.error(`Expected CDN files are missing:\n${missing.join("\n")}`); + process.exit(1); + } + + for (const filePath of filePaths) { + const template = fs.readFileSync(filePath, "utf8"); fs.writeFileSync( - path.resolve(`./examples/${example}/${relativePath}`), - updatedTemplate, + filePath, + template.replace( + /pspdfkit-web@([0-9]+.[0-9]+.[0-9]+)?/g, + `pspdfkit-web@${version}`, + ), ); } diff --git a/scripts/update-nutrient-in-examples.sh b/scripts/update-nutrient-in-examples.sh index a2c97906..2ea08276 100755 --- a/scripts/update-nutrient-in-examples.sh +++ b/scripts/update-nutrient-in-examples.sh @@ -3,6 +3,18 @@ set -euo pipefail SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +VERSION="${1:-}" + +if [ -z "${VERSION}" ]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +if ! [[ "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Expected a semver version, got \"${VERSION}\"." >&2 + exit 1 +fi + Green='\033[0;32m' Yellow='\033[0;33m' NoColor='\033[0m' @@ -12,16 +24,16 @@ upgrade_npm_in_example() { pushd "${SCRIPT_DIR}/../examples/${directory}/" > /dev/null - echo -e "\n${Green}Upgrading npm in ${Yellow}${directory}${Green} example${NoColor}" + echo -e "\n${Green}Upgrading ${Yellow}${directory}${Green} to ${Yellow}${VERSION}${NoColor}" if [ -f "pnpm-lock.yaml" ]; then - pnpm install @nutrient-sdk/viewer@latest --save --save-exact + pnpm install "@nutrient-sdk/viewer@${VERSION}" --save --save-exact pnpm install > /dev/null pnpm audit fix > /dev/null || true elif [ -f "package-lock.json" ]; then - npm install @nutrient-sdk/viewer@latest --save --save-exact + npm install "@nutrient-sdk/viewer@${VERSION}" --save --save-exact npm install > /dev/null @@ -30,7 +42,7 @@ upgrade_npm_in_example() { popd > /dev/null - node ./scripts/update-nutrient-in-cdn.js "${directory}" + node "${SCRIPT_DIR}/update-nutrient-in-cdn.js" "${directory}" "${VERSION}" } upgrade_npm_in_example "webpack" From 835de787a3e5daad25509154560fa65a53bdbc7f Mon Sep 17 00:00:00 2001 From: Ritesh Kumar Date: Thu, 27 Aug 2026 13:35:25 +0530 Subject: [PATCH 2/3] Open a pull request automatically on each SDK release Adds a daily workflow that checks the npm latest dist-tag, bumps every example, and opens a pull request. @PSPDFKit/web is requested for review through CODEOWNERS rather than by the workflow, so no token needs permission to resolve org teams. Detection refuses to act twice on the same release: it exits when the repository is already on the version, when the branch exists, or when a pull request for it was opened before, so a bump closed without merging is not reopened on the next run. Prereleases are rejected outright, so nightly builds never trigger a bump. Biome and the e2e suite run inside the job before the pull request is opened. A pull request created with GITHUB_TOKEN does not trigger the Biome or Playwright workflows, and main requires no status checks, so a bump would otherwise arrive with nothing having verified it. A failing suite still opens the pull request, as a draft, with the report attached. pnpm is pinned to 10 because pnpm 11 stops reading the pnpm.overrides field in package.json, which would silently drop the security overrides added in #98. --- .github/workflows/update-nutrient-sdk.yml | 140 ++++++++++++++++++++++ AGENTS.md | 5 + scripts/check-nutrient-update.sh | 56 +++++++++ 3 files changed, 201 insertions(+) create mode 100644 .github/workflows/update-nutrient-sdk.yml create mode 100755 scripts/check-nutrient-update.sh diff --git a/.github/workflows/update-nutrient-sdk.yml b/.github/workflows/update-nutrient-sdk.yml new file mode 100644 index 00000000..5369efb8 --- /dev/null +++ b/.github/workflows/update-nutrient-sdk.yml @@ -0,0 +1,140 @@ +name: Update Nutrient SDK + +on: + schedule: + # Daily, deliberately off the hour: GitHub deprioritises schedules that + # bunch on :00, which delays them further. + - cron: "17 6 * * *" + workflow_dispatch: + inputs: + version: + description: "Version to bump to. Defaults to the npm latest dist-tag." + required: false + type: string + +permissions: + contents: write + pull-requests: write + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + update: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Setup pnpm + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + with: + version: 10 + + - name: Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: .tool-versions + + - name: Check for a new SDK release + id: check + env: + GH_TOKEN: ${{ github.token }} + REQUESTED_VERSION: ${{ inputs.version }} + run: ./scripts/check-nutrient-update.sh "${REQUESTED_VERSION:-}" + + - name: Bump the SDK in every example + if: steps.check.outputs.should_update == 'true' + env: + VERSION: ${{ steps.check.outputs.version }} + run: ./scripts/update-nutrient-in-examples.sh "$VERSION" + + - name: Install root dependencies + if: steps.check.outputs.should_update == 'true' + run: pnpm install --frozen-lockfile + + - name: Format + if: steps.check.outputs.should_update == 'true' + run: pnpm run format + + - name: Install Playwright browsers + if: steps.check.outputs.should_update == 'true' + run: pnpm exec playwright install chromium --with-deps + + # A pull request opened with GITHUB_TOKEN does not trigger the Biome or + # Playwright workflows, and main requires no status checks, so the bump + # would otherwise arrive with no signal at all. Verify it here instead. + - name: Run e2e smoke tests + id: e2e + if: steps.check.outputs.should_update == 'true' + continue-on-error: true + run: pnpm run e2e-tests + + - name: Upload Playwright report + if: steps.check.outputs.should_update == 'true' && steps.e2e.outcome != 'success' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 + + - name: Commit and push the bump + if: steps.check.outputs.should_update == 'true' + env: + VERSION: ${{ steps.check.outputs.version }} + BRANCH: ${{ steps.check.outputs.branch }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + # Scoped to examples/: installing at the root can leave a stray + # package-lock.json, which is not gitignored. + git add examples/ + if git diff --cached --quiet; then + echo "Detection reported ${VERSION} was needed but nothing changed." >&2 + exit 1 + fi + git commit -m "Update examples with Nutrient SDK version $VERSION" + git push origin "$BRANCH" + + - name: Open the pull request + if: steps.check.outputs.should_update == 'true' + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.check.outputs.version }} + CURRENT: ${{ steps.check.outputs.current }} + BRANCH: ${{ steps.check.outputs.branch }} + E2E_OUTCOME: ${{ steps.e2e.outcome }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + if [ "$E2E_OUTCOME" = "success" ]; then + e2e_result="✅ passed" + draft="" + else + e2e_result="❌ failed — see the run log and the playwright-report artifact" + draft="--draft" + fi + + cat > /tmp/pr-body.md <&2 + exit 1 +fi + +current="$(jq -r '.dependencies["@nutrient-sdk/viewer"]' \ + "${REPO_ROOT}/examples/${REFERENCE_EXAMPLE}/package.json")" + +branch="update-examples-${latest}" +should_update="true" +reason="${current} -> ${latest}" + +if [ "${latest}" = "${current}" ]; then + should_update="false" + reason="already on ${latest}" +elif git -C "${REPO_ROOT}" ls-remote --exit-code --heads origin "${branch}" >/dev/null 2>&1; then + should_update="false" + reason="branch ${branch} already exists" +elif command -v gh >/dev/null 2>&1 && + [ -n "$(gh pr list --state all --head "${branch}" --json number --jq '.[].number' 2>/dev/null)" ]; then + # A closed-without-merge bump must not be reopened on every scheduled run. + should_update="false" + reason="a pull request for ${branch} already exists" +fi + +echo "should_update=${should_update} (${reason})" + +{ + echo "version=${latest}" + echo "current=${current}" + echo "branch=${branch}" + echo "should_update=${should_update}" +} >> "${GITHUB_OUTPUT:-/dev/stdout}" From 5564da1cbcde7aa68b881d063bb02cbe5fbec929 Mon Sep 17 00:00:00 2001 From: Ritesh Kumar Date: Thu, 27 Aug 2026 14:29:23 +0530 Subject: [PATCH 3/3] Harden the SDK bump automation against silent no-ops Refuse to reopen a closed bump, fail loudly when a run stops part-way, and reject a CDN map key or file that matches nothing. Exempt the salesforce README, whose version is an illustration rather than a pin. --- .github/workflows/update-nutrient-sdk.yml | 51 +++++++++++++++--- AGENTS.md | 11 ++-- scripts/check-nutrient-update.sh | 66 +++++++++++++++++------ scripts/update-nutrient-in-cdn.js | 46 ++++++++++------ scripts/update-nutrient-in-examples.sh | 3 ++ 5 files changed, 133 insertions(+), 44 deletions(-) diff --git a/.github/workflows/update-nutrient-sdk.yml b/.github/workflows/update-nutrient-sdk.yml index 5369efb8..1410e34b 100644 --- a/.github/workflows/update-nutrient-sdk.yml +++ b/.github/workflows/update-nutrient-sdk.yml @@ -22,8 +22,12 @@ concurrency: jobs: update: - timeout-minutes: 60 + timeout-minutes: 90 runs-on: ubuntu-latest + env: + # Root install runs `prepare`, which points core.hooksPath at .husky and + # would gate the bot's commit on lint-staged and the Biome version check. + HUSKY: "0" steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 @@ -51,6 +55,27 @@ jobs: VERSION: ${{ steps.check.outputs.version }} run: ./scripts/update-nutrient-in-examples.sh "$VERSION" + - name: Check every CDN reference was bumped + if: steps.check.outputs.should_update == 'true' + env: + VERSION: ${{ steps.check.outputs.version }} + run: | + # The CDN file map in update-nutrient-in-cdn.js is hand-maintained, so an + # example that gains a script tag without an entry is left behind in + # silence. examples/salesforce/README.md is exempt: its version is an + # illustration, not a pin. + stale="$(grep -rEn "pspdfkit-web@[0-9]+\.[0-9]+\.[0-9]+" examples/ \ + --exclude-dir=node_modules --exclude-dir=dist \ + --exclude-dir=.next --exclude-dir=.nuxt \ + | grep -v "pspdfkit-web@${VERSION}" \ + | grep -v '^examples/salesforce/README.md:' || true)" + + if [ -n "$stale" ]; then + echo "CDN references not updated to ${VERSION}:" >&2 + echo "$stale" >&2 + exit 1 + fi + - name: Install root dependencies if: steps.check.outputs.should_update == 'true' run: pnpm install --frozen-lockfile @@ -65,7 +90,7 @@ jobs: # A pull request opened with GITHUB_TOKEN does not trigger the Biome or # Playwright workflows, and main requires no status checks, so the bump - # would otherwise arrive with no signal at all. Verify it here instead. + # would otherwise arrive with no signal at all. - name: Run e2e smoke tests id: e2e if: steps.check.outputs.should_update == 'true' @@ -89,9 +114,9 @@ jobs: git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git checkout -b "$BRANCH" - # Scoped to examples/: installing at the root can leave a stray - # package-lock.json, which is not gitignored. - git add examples/ + # `pnpm run format` writes unsafe Biome fixes repository-wide, and a dev + # server may leave build output behind; neither belongs in a bump. + git add -u examples/ if git diff --cached --quiet; then echo "Detection reported ${VERSION} was needed but nothing changed." >&2 exit 1 @@ -108,12 +133,13 @@ jobs: BRANCH: ${{ steps.check.outputs.branch }} E2E_OUTCOME: ${{ steps.e2e.outcome }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + BASE: ${{ github.ref_name }} run: | if [ "$E2E_OUTCOME" = "success" ]; then e2e_result="✅ passed" draft="" else - e2e_result="❌ failed — see the run log and the playwright-report artifact" + e2e_result="❌ failed. The failing examples are named at the end of the run log." draft="--draft" fi @@ -126,7 +152,7 @@ jobs: pull request created by \`GITHUB_TOKEN\` does not trigger the Biome or Playwright workflows, so the checks tab here will be empty. - - Formatting (\`pnpm run format\`): applied + - Formatting (\`pnpm run format\`): applied to \`examples/\` - E2E smoke tests (\`pnpm run e2e-tests\`): ${e2e_result} [Workflow run](${RUN_URL}) @@ -135,6 +161,15 @@ jobs: gh pr create \ --title "Update examples with Nutrient SDK version $VERSION" \ --body-file /tmp/pr-body.md \ - --base main \ + --base "$BASE" \ --head "$BRANCH" \ $draft + + - name: Fail the run when the e2e suite failed + if: steps.check.outputs.should_update == 'true' && steps.e2e.outcome != 'success' + env: + VERSION: ${{ steps.check.outputs.version }} + run: | + echo "::error::E2E failed for ${VERSION}. The pull request was opened as a draft," + echo "::error::and GitHub does not request code owners on drafts." + exit 1 diff --git a/AGENTS.md b/AGENTS.md index 78586898..c8ce52de 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,7 +87,7 @@ SERVER_DIR=examples/javascript-vite npm run test npm run audit-fix # Bump Nutrient SDK version in all examples (version is required) -npm run update-nutrient-version -- 1.21.0 +npm run update-nutrient-version -- ``` ## Adding a New Example @@ -126,9 +126,12 @@ svelte-kit, vue-composition-api. - **Biome** — Code formatting check on every push/PR - **Playwright** — Smoke tests on push/PR to main (installs all deps, runs e2e) - **Update Nutrient SDK** — Daily check for a new `@nutrient-sdk/viewer` release. - Bumps every example, runs Biome and the e2e suite inside the job, then opens a - PR (draft if e2e failed). `@PSPDFKit/web` is requested via CODEOWNERS. Can be - run on demand via `workflow_dispatch`, optionally against a specific version. + Bumps every example listed in `update-nutrient-in-examples.sh`, formats with + Biome, runs the e2e suite inside the job, then opens a PR. A passing suite + opens it ready for review, so CODEOWNERS requests `@PSPDFKit/web`; a failing + one opens it as a draft and fails the run, because GitHub does not request + code owners on drafts. Can be run on demand via `workflow_dispatch`, + optionally against a specific version. ## Code Style diff --git a/scripts/check-nutrient-update.sh b/scripts/check-nutrient-update.sh index f788950b..9b177a5a 100755 --- a/scripts/check-nutrient-update.sh +++ b/scripts/check-nutrient-update.sh @@ -1,13 +1,12 @@ #!/bin/bash -# Decides whether a Nutrient SDK bump is needed, and refuses to start one that -# is already in flight. Writes version/branch/should_update to $GITHUB_OUTPUT. +# Decides whether a Nutrient SDK bump is needed, refusing one already in flight. set -euo pipefail SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" REPO_ROOT="${SCRIPT_DIR}/.." -# Every example tracked by update-nutrient-in-examples.sh is held at the same -# version, so any one of them reports the version the repository is on. +# Every example is held at the same version, so one of them reports the version +# the repository is on. REFERENCE_EXAMPLE="react" DIST_TAGS_URL="https://registry.npmjs.org/-/package/@nutrient-sdk/viewer/dist-tags" @@ -17,7 +16,7 @@ requested="${1:-}" if [ -n "${requested}" ]; then latest="${requested}" else - latest="$(curl -fsSL --retry 3 --retry-delay 2 "${DIST_TAGS_URL}" | jq -r '.latest')" + latest="$(curl -fsSL --retry 3 --retry-delay 2 "${DIST_TAGS_URL}" | jq -er '.latest')" fi # Anything carrying a prerelease suffix is a nightly, never a release we ship. @@ -26,24 +25,57 @@ if ! [[ "${latest}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then exit 1 fi -current="$(jq -r '.dependencies["@nutrient-sdk/viewer"]' \ - "${REPO_ROOT}/examples/${REFERENCE_EXAMPLE}/package.json")" +# jq -r prints "null" and exits 0 for a missing key, which would silently +# disable the already-on-this-version check. +if ! current="$(jq -er '.dependencies["@nutrient-sdk/viewer"]' \ + "${REPO_ROOT}/examples/${REFERENCE_EXAMPLE}/package.json")"; then + echo "examples/${REFERENCE_EXAMPLE} has no @nutrient-sdk/viewer dependency." >&2 + echo "The reference example moved or was renamed; update REFERENCE_EXAMPLE." >&2 + exit 1 +fi branch="update-examples-${latest}" -should_update="true" -reason="${current} -> ${latest}" if [ "${latest}" = "${current}" ]; then should_update="false" reason="already on ${latest}" -elif git -C "${REPO_ROOT}" ls-remote --exit-code --heads origin "${branch}" >/dev/null 2>&1; then - should_update="false" - reason="branch ${branch} already exists" -elif command -v gh >/dev/null 2>&1 && - [ -n "$(gh pr list --state all --head "${branch}" --json number --jq '.[].number' 2>/dev/null)" ]; then - # A closed-without-merge bump must not be reopened on every scheduled run. - should_update="false" - reason="a pull request for ${branch} already exists" +else + # --state all: a bump closed without merging must not be reopened on every + # scheduled run. A failed lookup must not read as "no pull request". + if ! pull_requests="$(gh pr list --state all --head "${branch}" \ + --json number --jq '.[].number')"; then + echo "Could not list pull requests for ${branch}; refusing to guess." >&2 + exit 1 + fi + + if [ -n "${pull_requests}" ]; then + should_update="false" + reason="a pull request for ${branch} already exists" + else + ls_remote_status=0 + git -C "${REPO_ROOT}" ls-remote --exit-code --heads origin "${branch}" \ + >/dev/null || ls_remote_status=$? + + case "${ls_remote_status}" in + # An earlier run pushed the branch and then failed before opening its + # pull request. Skipping quietly would bury this version for good. + 0) + echo "Branch ${branch} exists with no pull request, so an earlier run" >&2 + echo "failed part-way. Delete it or open its pull request by hand." >&2 + exit 1 + ;; + # --exit-code reserves 2 for "no matching ref"; anything else is a real + # failure that must not read as "the branch is free". + 2) + should_update="true" + reason="${current} -> ${latest}" + ;; + *) + echo "git ls-remote failed with status ${ls_remote_status}." >&2 + exit 1 + ;; + esac + fi fi echo "should_update=${should_update} (${reason})" diff --git a/scripts/update-nutrient-in-cdn.js b/scripts/update-nutrient-in-cdn.js index 9a6b41f2..a139c843 100644 --- a/scripts/update-nutrient-in-cdn.js +++ b/scripts/update-nutrient-in-cdn.js @@ -5,15 +5,16 @@ const cdnOcurrences = { gatsbyjs: ["src/templates/Viewport.js"], "javascript-vite": ["index.html"], nuxtjs: ["components/NutrientContainer.vue"], - salesforce: [ - "README.md", - "force-app/main/default/pages/Nutrient_InitNutrient.page", - ], + salesforce: ["force-app/main/default/pages/Nutrient_InitNutrient.page"], typescript: ["src/index.html"], "typescript-vite": ["index.html"], webpack: ["README.md", "src/index.html"], }; +const CDN_VERSION = /pspdfkit-web@\d+\.\d+\.\d+/g; + +const examplesDir = path.resolve(__dirname, "..", "examples"); + const example = process.argv[2]; const version = process.argv[3]; @@ -29,15 +30,24 @@ if (!/^\d+\.\d+\.\d+$/.test(version)) { process.exit(1); } -if (cdnOcurrences[example]) { +// A key matching no directory is never looked up, so a typo in one is silent. +const strayKeys = Object.keys(cdnOcurrences).filter( + (key) => !fs.existsSync(path.resolve(examplesDir, key)), +); + +if (strayKeys.length > 0) { + console.error(`Keys matching no example directory: ${strayKeys.join(", ")}`); + process.exit(1); +} + +if (Object.hasOwn(cdnOcurrences, example)) { console.log(`Updating CDN version in ${example} example.`); const filePaths = cdnOcurrences[example].map((relativePath) => - path.resolve(__dirname, "..", "examples", example, relativePath), + path.resolve(examplesDir, example, relativePath), ); - // Validate every file up front: a renamed or moved file would otherwise be - // skipped silently, or abort the run half-written across the example. + // Up front: a renamed file would otherwise abort the run half-written. const missing = filePaths.filter((filePath) => !fs.existsSync(filePath)); if (missing.length > 0) { @@ -47,14 +57,20 @@ if (cdnOcurrences[example]) { for (const filePath of filePaths) { const template = fs.readFileSync(filePath, "utf8"); + let replacements = 0; + + const updated = template.replace(CDN_VERSION, () => { + replacements += 1; + return `pspdfkit-web@${version}`; + }); + + // A zero-match replace rewrites the file byte-identical, reporting success. + if (replacements === 0) { + console.error(`No versioned CDN reference found in ${filePath}.`); + process.exit(1); + } - fs.writeFileSync( - filePath, - template.replace( - /pspdfkit-web@([0-9]+.[0-9]+.[0-9]+)?/g, - `pspdfkit-web@${version}`, - ), - ); + fs.writeFileSync(filePath, updated); } console.log(`Updated CDN version in ${example} example.`); diff --git a/scripts/update-nutrient-in-examples.sh b/scripts/update-nutrient-in-examples.sh index 2ea08276..cf0eb1e8 100755 --- a/scripts/update-nutrient-in-examples.sh +++ b/scripts/update-nutrient-in-examples.sh @@ -38,6 +38,9 @@ upgrade_npm_in_example() { npm install > /dev/null npm audit fix > /dev/null || true + else + echo "examples/${directory} has no lockfile, so nothing would be installed." >&2 + exit 1 fi popd > /dev/null