From 9a1ee9a144c5ee959d86b903fbfd0bc1861ec897 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Wed, 3 Dec 2025 16:26:37 -0800 Subject: [PATCH 01/18] Use `npm clean-install` in CI The `--no-package-lock` workaround was added due to npm bug #4828, where npm < 11.3.0 generates incomplete lockfiles for packages with optional platform dependencies (esbuild, rollup). Optional cross-platform dependencies were restored to `package-lock.json` in 358f276b, so npm will be able to install from the lock file in the GitHub Actions. Also, fixed in npm 11.3.0 (Apr 2025), but Node v22 ships npm v10 and will remain affected out-of-the-box. Investigation notes follow. What happened? -------------- 1. Switch from yarn to npm: `package-lock.json` added, `yarn.lock` removed - https://github.com/modelcontextprotocol/inspector/commit/702f827f74786acd7bd29fd9c389d701ed902a23 Presumably: - run `npm install` to generate a `package-lock.json` from the yarn-managed `node_modules`, on macos - bug #4828: npm omitted optional cross-platform dependencies from the lock file npm can't easily fix this. `npm install foo@x.y.z` could write a resolved dependency, but at the exact version so `foo` would be pinned, and semver operator twiddling would be required to restore it as-was. The fix would be to "manually" re-add the missing metadata. 2. Pull 47, tries `npm ci`, and reverts, on 11 Nov 2024 https://github.com/modelcontextprotocol/inspector/pull/47/commits/3789ef984a2d2dfbef4e2455f21995893cf463d9 - "Try restoring npm ci" --> testing the new node release for the bug? - ran against `setup-node`, `node-version: 18`, likely: 18.20.5 (released nov 11, 2024; ~same day) - git show 3789ef984a2d2dfbef4e2455f21995893cf463d9:.github/workflows/main.yml - Failed action, and logs have expired - https://github.com/modelcontextprotocol/inspector/actions/runs/11782443393/job/32817472448 - https://nodejs.org/en/download/archive/v18.20.5 - uses npm 10.8.2 - Re-tried in inspector fork - workflow run at 3789ef984a2d2dfbef4e2455f21995893cf463d9 - change `node-version: 18` to `18.20.5` (exact node / npm on commit date) - Fails due to missing `linux-x64-gnu` platform dep (rollup, would similarly affect esbuild) 3. Cross-platform dependencies restored to lockfile on 1 May 2025 https://github.com/modelcontextprotocol/inspector/pull/372 - https://github.com/modelcontextprotocol/inspector/commit/358f276b9bd559638f41aedddacca1000642a4c0 - worked because `package-lock.json` and `node_modules` were both removed - i.e., not the bug conditions -> even npm < 11.3.0 generates correct lockfile - At that point, `--no-package-lock` could've been removed from CI, Dockerfile, etc. NPM --- npm (aborist) fixed #4828 https://github.com/npm/cli/pull/8184 - https://github.com/npm/cli/issues/4828 --> frequent http 500, due to many comments - https://github.com/npm/cli/commit/a96d8f6295886c219076178460718837d2fe45d6 - will not be backported Released in 11.3.0 on 8 Apr https://github.com/npm/cli/pull/8150 - https://github.com/npm/cli/releases/tag/v11.3.0 - arborist 9.0.2 - https://github.com/npm/cli/releases/tag/arborist-v9.0.2 - npm v11.3.0 ships with node v24.2.0, on 6 May 2025 - https://nodejs.org/en/download/archive/v24 Node v22 ships npm v10 - https://nodejs.org/en/download/archive/v22 - will always be affected, no backport coming --- .github/workflows/main.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d01f7175b..4527bc95a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,9 +22,7 @@ jobs: node-version-file: package.json cache: npm - # Working around https://github.com/npm/cli/issues/4828 - # - run: npm ci - - run: npm install --no-package-lock + - run: npm clean-install - name: Check version consistency run: npm run check-version @@ -57,9 +55,7 @@ jobs: cache: npm registry-url: "https://registry.npmjs.org" - # Working around https://github.com/npm/cli/issues/4828 - # - run: npm ci - - run: npm install --no-package-lock + - run: npm clean-install # TODO: Add --provenance once the repo is public - run: npm run publish-all From e259f7c3b254f135019e722ab2874ada0db5f4e8 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sat, 29 Nov 2025 20:38:23 -0800 Subject: [PATCH 02/18] Use package script for prettier and lint Top-level `npm run lint` runs `prettier --check .` and lint in client/ Using the package script after install ensures the same procedure and versions are used in CI and development, and also avoids yet-another variant of a direct prettier invocation. --- .github/workflows/main.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4527bc95a..f1e7d94bb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,9 +14,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Check formatting - run: npx prettier --check . - - uses: actions/setup-node@v4 with: node-version-file: package.json @@ -27,8 +24,7 @@ jobs: - name: Check version consistency run: npm run check-version - - name: Check linting - working-directory: ./client + - name: Check formatting and linting run: npm run lint - name: Run client tests From 9952f01dcb3b8303234e472784a76c17979b6a5c Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Tue, 2 Dec 2025 15:44:15 -0800 Subject: [PATCH 03/18] Whitespace --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f1e7d94bb..7632c0769 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,6 +4,7 @@ on: - main pull_request: + release: types: [published] @@ -45,6 +46,7 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version-file: package.json @@ -63,11 +65,13 @@ jobs: if: github.event_name == 'release' environment: release needs: build + permissions: contents: read packages: write attestations: write id-token: write + steps: - uses: actions/checkout@v4 From 5ddccba61e93367f335ce78cb4a7e1056cdfbc2a Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Sat, 29 Nov 2025 21:45:00 -0800 Subject: [PATCH 04/18] Name the `main.yml` workflow for the GitHub Actions UI --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7632c0769..04b592991 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,3 +1,5 @@ +name: Build and release + on: push: branches: From 81372368fc8acee8bf8ce67eda0d5e2e1b266bb7 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Tue, 2 Dec 2025 15:44:55 -0800 Subject: [PATCH 05/18] Dependency range test workflow without lockfile to test semver --- .github/workflows/dependency-range-test.yml | 42 +++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/dependency-range-test.yml diff --git a/.github/workflows/dependency-range-test.yml b/.github/workflows/dependency-range-test.yml new file mode 100644 index 000000000..4335afa41 --- /dev/null +++ b/.github/workflows/dependency-range-test.yml @@ -0,0 +1,42 @@ +# When users `npm install` the Inspector, npm will try to flatten/deduplicate dependencies across +# their entire project, and could choose newer versions than our lock file (within our semver +# constraints). This test helps catch when those newer versions break the build. + +name: Dependency Range Test + +on: + push: + branches: + - main + + pull_request: + +jobs: + dependency-range-test: + runs-on: ubuntu-latest + + # Don't block if this fails - it's informational + continue-on-error: true + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: package.json + # Omit cache to test with freshly-resolved dependencies + + - name: Install dependencies with fresh resolution + run: npm install --no-package-lock + + - name: Check version consistency + run: npm run check-version + + - name: Check formatting and linting + run: npm run lint + + - name: Run client tests + working-directory: ./client + run: npm test + + - run: npm run build From 338505ae4a3f7a0ce5abc779baa19f664e13ca64 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Tue, 2 Dec 2025 19:49:20 -0800 Subject: [PATCH 06/18] Consistent branch YAML notation in the E2E workflow --- .github/workflows/e2e_tests.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 378905b44..7e56736ca 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -2,9 +2,11 @@ name: Playwright Tests on: push: - branches: [main] + branches: + - main pull_request: - branches: [main] + branches: + - main jobs: test: @@ -67,11 +69,10 @@ jobs: with: create-comment: ${{ github.event.pull_request.head.repo.full_name == github.repository }} report-file: client/results.json - comment-title: "🎭 Playwright E2E Test Results" - job-summary: true - icon-style: "emojis" + comment-title: "Playwright test results" custom-info: | - **Test Environment:** Ubuntu Latest, Node.js ${{ steps.setup_node.outputs.node-version }} + + **Environment:** Ubuntu Latest, Node.js ${{ steps.setup_node.outputs.node-version }} **Browsers:** Chromium, Firefox 📊 [View Detailed HTML Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) (download artifacts) From 8c7571f6947faa5aea54e75de6401e42df810e5a Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Wed, 3 Dec 2025 16:54:54 -0800 Subject: [PATCH 07/18] Move publish jobs to dedicated release workflow Separates publish jobs into `release.yml` to eliminate "skipped" status noise on pull requests. All PRs currently display "skipped" for publish jobs, which is correct since they should only run on releases. However, this can confuse PR submitters who may not realize the skipping is expected rather than a failure related to their changes (accidental release trigger, etc.). Separate workflows keep PR checks focused on build and test jobs relevant to the submitter. --- .github/workflows/build.yml | 37 +++++++++++++++++++++ .github/workflows/{main.yml => release.yml} | 34 ++----------------- 2 files changed, 39 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/build.yml rename .github/workflows/{main.yml => release.yml} (75%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..07bd8a66e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,37 @@ +name: Build + +on: + push: + branches: + - main + + pull_request: + + # Allow `uses: ...` in other workflows, used by `release.yml` + workflow_call: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: package.json + cache: npm + + - run: npm clean-install + + - name: Check version consistency + run: npm run check-version + + - name: Check formatting and linting + run: npm run lint + + - name: Run client tests + working-directory: ./client + run: npm test + + - run: npm run build diff --git a/.github/workflows/main.yml b/.github/workflows/release.yml similarity index 75% rename from .github/workflows/main.yml rename to .github/workflows/release.yml index 04b592991..3996ee3f7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/release.yml @@ -1,44 +1,15 @@ -name: Build and release +name: Release on: - push: - branches: - - main - - pull_request: - release: types: [published] jobs: build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version-file: package.json - cache: npm - - - run: npm clean-install - - - name: Check version consistency - run: npm run check-version - - - name: Check formatting and linting - run: npm run lint - - - name: Run client tests - working-directory: ./client - run: npm test - - - run: npm run build + uses: ./.github/workflows/build.yml publish: runs-on: ubuntu-latest - if: github.event_name == 'release' environment: release needs: build @@ -64,7 +35,6 @@ jobs: publish-github-container-registry: runs-on: ubuntu-latest - if: github.event_name == 'release' environment: release needs: build From 088e9af49f1a219de85bd275a2114e82859c4d5f Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Thu, 4 Dec 2025 12:13:19 -0800 Subject: [PATCH 08/18] Document `published` vs. `released` and `prereleased` --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3996ee3f7..5c2332b2e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,7 @@ name: Release on: release: + # `published` triggers for both `released` and `prereleased`, even from a saved draft types: [published] jobs: From 9a67075f81c196f382e3cc2de1d176f75177d8c0 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Thu, 4 Dec 2025 12:16:41 -0800 Subject: [PATCH 09/18] Move dependency range test job into build - Shows all jobs related to a PR in one workflow run for convenience - Fewer "duplicate" runs, de-cluttering the Github Actions UI - Conceptual grouping since it uses the same triggers as build job --- .github/workflows/build.yml | 32 ++++++++++++++++ .github/workflows/dependency-range-test.yml | 42 --------------------- 2 files changed, 32 insertions(+), 42 deletions(-) delete mode 100644 .github/workflows/dependency-range-test.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 07bd8a66e..473f8496d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,3 +35,35 @@ jobs: run: npm test - run: npm run build + + # When users `npm install` the Inspector, npm will try to flatten/deduplicate dependencies across + # their entire project, and could choose newer versions than our lock file (within our semver + # constraints). This test helps catch when those newer versions break the build. + dependency-range-test: + runs-on: ubuntu-latest + + # Don't block if this fails - it's informational + continue-on-error: true + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: package.json + # Omit cache to test with freshly-resolved dependencies + + - name: Install dependencies with fresh resolution + run: npm install --no-package-lock + + - name: Check version consistency + run: npm run check-version + + - name: Check formatting and linting + run: npm run lint + + - name: Run client tests + working-directory: ./client + run: npm test + + - run: npm run build diff --git a/.github/workflows/dependency-range-test.yml b/.github/workflows/dependency-range-test.yml deleted file mode 100644 index 4335afa41..000000000 --- a/.github/workflows/dependency-range-test.yml +++ /dev/null @@ -1,42 +0,0 @@ -# When users `npm install` the Inspector, npm will try to flatten/deduplicate dependencies across -# their entire project, and could choose newer versions than our lock file (within our semver -# constraints). This test helps catch when those newer versions break the build. - -name: Dependency Range Test - -on: - push: - branches: - - main - - pull_request: - -jobs: - dependency-range-test: - runs-on: ubuntu-latest - - # Don't block if this fails - it's informational - continue-on-error: true - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version-file: package.json - # Omit cache to test with freshly-resolved dependencies - - - name: Install dependencies with fresh resolution - run: npm install --no-package-lock - - - name: Check version consistency - run: npm run check-version - - - name: Check formatting and linting - run: npm run lint - - - name: Run client tests - working-directory: ./client - run: npm test - - - run: npm run build From 3b46ac8fbbc38484f6b2bcd7ed1950cf0ba64cb9 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Tue, 2 Dec 2025 20:22:15 -0800 Subject: [PATCH 10/18] Move E2E test job into build - Shows all jobs related to a PR in one workflow run for convenience - Fewer "duplicate" runs, de-cluttering the Github Actions UI - Conceptual grouping since it uses the same triggers as build job --- .github/workflows/build.yml | 69 ++++++++++++++++++++++++++++ .github/workflows/e2e_tests.yml | 79 --------------------------------- 2 files changed, 69 insertions(+), 79 deletions(-) delete mode 100644 .github/workflows/e2e_tests.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 473f8496d..9c96e3a42 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,3 +67,72 @@ jobs: run: npm test - run: npm run build + + e2e-tests: + # Installing Playwright dependencies can take quite awhile, and also depends on GitHub CI load. + timeout-minutes: 15 + runs-on: ubuntu-latest + + steps: + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libwoff1 + + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + id: setup_node + with: + node-version-file: package.json + cache: npm + + # Cache Playwright browsers + - name: Cache Playwright browsers + id: cache-playwright + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright # The default Playwright cache path + key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} # Cache key based on OS and package-lock.json + restore-keys: | + ${{ runner.os }}-playwright- + + - name: Install dependencies + run: npm ci + + - name: Install Playwright dependencies + run: npx playwright install-deps + + - name: Install Playwright and browsers unless cached + run: npx playwright install --with-deps + if: steps.cache-playwright.outputs.cache-hit != 'true' + + - name: Run Playwright tests + id: playwright-tests + run: npm run test:e2e + + - name: Upload Playwright Report and Screenshots + uses: actions/upload-artifact@v4 + if: steps.playwright-tests.conclusion != 'skipped' + with: + name: playwright-report + path: | + client/playwright-report/ + client/test-results/ + client/results.json + retention-days: 2 + + - name: Publish Playwright Test Summary + uses: daun/playwright-report-summary@v3 + if: steps.playwright-tests.conclusion != 'skipped' + with: + create-comment: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + report-file: client/results.json + comment-title: "Playwright test results" + custom-info: | + + **Environment:** Ubuntu Latest, Node.js ${{ steps.setup_node.outputs.node-version }} + **Browsers:** Chromium, Firefox + + 📊 [View Detailed HTML Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) (download artifacts) + test-command: "npm run test:e2e" diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml deleted file mode 100644 index 7e56736ca..000000000 --- a/.github/workflows/e2e_tests.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Playwright Tests - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - test: - # Installing Playwright dependencies can take quite awhile, and also depends on GitHub CI load. - timeout-minutes: 15 - runs-on: ubuntu-latest - - steps: - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y libwoff1 - - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - id: setup_node - with: - node-version-file: package.json - cache: npm - - # Cache Playwright browsers - - name: Cache Playwright browsers - id: cache-playwright - uses: actions/cache@v4 - with: - path: ~/.cache/ms-playwright # The default Playwright cache path - key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} # Cache key based on OS and package-lock.json - restore-keys: | - ${{ runner.os }}-playwright- - - - name: Install dependencies - run: npm ci - - - name: Install Playwright dependencies - run: npx playwright install-deps - - - name: Install Playwright and browsers unless cached - run: npx playwright install --with-deps - if: steps.cache-playwright.outputs.cache-hit != 'true' - - - name: Run Playwright tests - id: playwright-tests - run: npm run test:e2e - - - name: Upload Playwright Report and Screenshots - uses: actions/upload-artifact@v4 - if: steps.playwright-tests.conclusion != 'skipped' - with: - name: playwright-report - path: | - client/playwright-report/ - client/test-results/ - client/results.json - retention-days: 2 - - - name: Publish Playwright Test Summary - uses: daun/playwright-report-summary@v3 - if: steps.playwright-tests.conclusion != 'skipped' - with: - create-comment: ${{ github.event.pull_request.head.repo.full_name == github.repository }} - report-file: client/results.json - comment-title: "Playwright test results" - custom-info: | - - **Environment:** Ubuntu Latest, Node.js ${{ steps.setup_node.outputs.node-version }} - **Browsers:** Chromium, Firefox - - 📊 [View Detailed HTML Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) (download artifacts) - test-command: "npm run test:e2e" From 485081d93680c64e8208ae2057af03f3789a0aea Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Thu, 4 Dec 2025 16:15:00 -0800 Subject: [PATCH 11/18] [Opt] Add lockfile validation --- .github/workflows/build.yml | 2 + scripts/validate-platform-dependencies.js | 361 ++++++++++++++++++++++ 2 files changed, 363 insertions(+) create mode 100644 scripts/validate-platform-dependencies.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9c96e3a42..723e4371c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,6 +22,8 @@ jobs: node-version-file: package.json cache: npm + - run: node scripts/validate-platform-dependencies.js + - run: npm clean-install - name: Check version consistency diff --git a/scripts/validate-platform-dependencies.js b/scripts/validate-platform-dependencies.js new file mode 100644 index 000000000..bcf47d0d3 --- /dev/null +++ b/scripts/validate-platform-dependencies.js @@ -0,0 +1,361 @@ +/* + * Validates `package-lock.json` contains resolution metadata + * (resolved/integrity) for ALL optional platform-specific dependencies, not + * just the current platform. + * + * WHY: + * + * npm < 11.3.0 has a bug (https://github.com/npm/cli/issues/4828) where + * running `npm install` under specific conditions generates a lockfile that + * includes optional dependencies but omits resolution metadata for + * non-current platforms. + * + * NOTE: npm 11.3.0+ (Apr 8, 2025) fixes this bug. + * + * BUG CONDITIONS: + * + * - no `package-lock.json` + * - `node_modules` exists with packages for current platform + * + * When `npm install` runs in this state, it includes all platform-specific + * optional dependencies, but only resolves them for the current platform. + * Other platforms remain unresolved. + * + * This breaks cross-platform compatibility - when developers on different + * OSes or CI systems run `npm install`, npm skips installing the unresolved + * platform-specific dependencies for their platform. + * + * SCENARIOS: + * + * - Changing package managers (e.g., yarn → npm use different lock files) + * + * - Resolving complex `package-lock.json` merge conflicts by deleting and + * regenerating (generally better to fix conflicts on respective branches) + * + * USAGE: + * + * `node scripts/validate-platform-dependencies.js` + * + * Exits with error if cross-platform resolution metadata cannot be found. + * + * Suggests fixes: + * + * 1. Upgrade to npm >= 11.3.0 and regenerate lockfile (preferred) + * 2. Run with --add-missing to fetch from registry + * + */ + +import fs from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { execSync } from "node:child_process"; + +const LOCKFILE_NAME = "package-lock.json"; + +// ANSI color codes +const COLORS = { + RED: "\x1b[31m", + RESET: "\x1b[0m", +}; + +async function main() { + const cwd = process.cwd(); + + // Parse command line arguments + const args = process.argv.slice(2); + const addMissing = args.includes("--add-missing"); + + // Parse --lockfile argument + const lockfileArg = args.find((arg) => arg.startsWith("--lockfile=")); + const lockPath = lockfileArg + ? path.resolve(cwd, lockfileArg.split("=")[1]) + : path.resolve(cwd, LOCKFILE_NAME); + + if (!fs.existsSync(lockPath)) { + console.error(`❌ No lockfile found at ${lockPath}`); + process.exit(1); + } + + const lockfile = JSON.parse(fs.readFileSync(lockPath, "utf8")); + const packages = lockfile.packages || {}; + + // 1. Build the Dependency Graph & Index Resolved Names + const resolvedNames = new Set(); + const parentMap = new Map(); + + Object.entries(packages).forEach(([pkgPath, entry]) => { + // 1a. Index names + if (pkgPath !== "") { + const name = getPackageNameFromPath(pkgPath); + if (name) resolvedNames.add(name); + } + + // 1b. Build Parent Map (Who depends on me?) + // Must include all dependency types to ensure we can trace the graph + const allDeps = { + ...entry.dependencies, + ...entry.devDependencies, + ...entry.peerDependencies, + ...entry.optionalDependencies, + }; + + Object.keys(allDeps).forEach((depName) => { + // Basic hoisting resolution logic + let childPath = `node_modules/${depName}`; + + // Check for nested resolution (shadowing) + const nestedPath = `${pkgPath}/node_modules/${depName}`; + if (packages[nestedPath]) { + childPath = nestedPath; + } + + if (!parentMap.has(childPath)) { + parentMap.set(childPath, new Set()); + } + parentMap.get(childPath).add(pkgPath); + }); + }); + + // 2. Identify Broken Packages + const brokenPackages = new Set(); + + Object.entries(packages).forEach(([pkgPath, entry]) => { + if (!entry.optionalDependencies) return; + + Object.keys(entry.optionalDependencies).forEach((depName) => { + if (!resolvedNames.has(depName)) { + brokenPackages.add(pkgPath); + } + }); + }); + + // 3. Trace back to Workspace Roots (The Fix) + const fixes = new Map(); + + brokenPackages.forEach((brokenPath) => { + const rootOwner = traceToWorkspace(brokenPath, parentMap); + + if (rootOwner) { + const { workspace, directDependency } = rootOwner; + + if (!fixes.has(workspace)) { + fixes.set(workspace, new Map()); + } + + // We reinstall the DIRECT DEPENDENCY (e.g. vite), not the broken child (esbuild) + const depEntry = packages[directDependency]; + const name = getPackageNameFromPath(directDependency); + + if (name && depEntry && depEntry.version) { + fixes.get(workspace).set(name, depEntry.version); + } + } + }); + + // 4. Report or fix missing platform dependencies + if (fixes.size === 0) { + console.log("✅ All platform-specific dependencies are properly resolved."); + return; + } + + console.log(`${COLORS.RED}%s${COLORS.RESET}\n`, "⚠️ MISSING PACKAGES"); + console.log( + "Resolution metadata is missing for cross-platform optional dependencies.\n", + ); + + // Find all missing optional dependencies + const missingPackages = new Map(); // packageName@version -> [parent paths] + + brokenPackages.forEach((brokenPath) => { + const entry = packages[brokenPath]; + if (!entry.optionalDependencies) return; + + Object.keys(entry.optionalDependencies).forEach((depName) => { + if (!resolvedNames.has(depName)) { + const version = entry.optionalDependencies[depName]; + const key = `${depName}@${version}`; + + if (!missingPackages.has(key)) { + missingPackages.set(key, []); + } + missingPackages.get(key).push(brokenPath); + } + }); + }); + + console.log(`${missingPackages.size} missing package(s):\n`); + + // Show which top-level packages depend on the broken ones + console.log("📦 Top-level dependencies requiring these packages:\n"); + + // Check if there are workspaces (more than just root, or any non-root workspace) + const hasWorkspaces = fixes.size > 1 || (fixes.size === 1 && !fixes.has("")); + + for (const [wsPath, pkgMap] of fixes.entries()) { + const pkgs = Array.from(pkgMap.entries()) + .map(([name, ver]) => `${name}@${ver}`) + .join(", "); + + if (hasWorkspaces) { + const wsName = wsPath === "" ? "root" : wsPath; + console.log(` [${wsName}]: ${pkgs}`); + } else { + // No workspaces - just list packages without [root] prefix + console.log(` ${pkgs}`); + } + } + + // Group by package family for cleaner output + console.log("\n🔍 Missing packages by family:\n"); + const packageFamilies = new Map(); + for (const pkgSpec of missingPackages.keys()) { + const [name] = + pkgSpec.split("@").filter(Boolean).length === 2 + ? pkgSpec.match(/^(@?[^@]+)@(.+)$/).slice(1) + : [pkgSpec, ""]; + const family = name.split("/")[0]; + if (!packageFamilies.has(family)) { + packageFamilies.set(family, []); + } + packageFamilies.get(family).push(pkgSpec); + } + + for (const [family, packages] of packageFamilies.entries()) { + console.log(` ${family}:`); + packages.forEach((pkg) => console.log(` - ${pkg}`)); + console.log(""); + } + + if (!addMissing) { + // Report mode - just show what's missing and how to fix + console.log("\n📋 Actions you can take:\n"); + console.log( + " 1. Run this script with `--add-missing` to automatically fetch and add entries:\n", + ); + console.log( + " `node validate-platform-dependencies.js --add-missing`\n\n", + ); + console.log( + " 2. Upgrade to npm >= 11.3.0 which has a fix for this issue, and regenerate the lockfile\n", + ); + process.exit(1); + } + + // Add missing mode - fetch from registry and add to lockfile + console.log("\n🔧 Fetching missing packages from npm registry...\n"); + + let addedCount = 0; + + for (const [pkgSpec, parents] of missingPackages.entries()) { + const [name, version] = + pkgSpec.split("@").filter(Boolean).length === 2 + ? pkgSpec.match(/^(@?[^@]+)@(.+)$/).slice(1) + : [pkgSpec, ""]; + + console.log(` Fetching ${name}@${version}...`); + + try { + // Fetch package metadata from npm registry + const url = `https://registry.npmjs.org/${name}/${version}`; + const response = await fetch(url); + + if (!response.ok) { + console.error( + ` ❌ Failed to fetch: ${response.status} ${response.statusText}`, + ); + continue; + } + + const data = await response.json(); + + // Construct lockfile entry + const pkgPath = `node_modules/${name}`; + + packages[pkgPath] = { + version: data.version, + resolved: data.dist.tarball, + integrity: data.dist.integrity, + cpu: data.cpu || undefined, + license: data.license || undefined, + optional: true, + os: data.os || undefined, + engines: data.engines || undefined, + }; + + // Clean up undefined fields + Object.keys(packages[pkgPath]).forEach((key) => { + if (packages[pkgPath][key] === undefined) { + delete packages[pkgPath][key]; + } + }); + + console.log(` ✓ Added ${pkgPath}`); + addedCount++; + } catch (error) { + console.error(` ❌ Error fetching ${name}@${version}:`, error.message); + } + } + + if (addedCount > 0) { + // Write updated lockfile + console.log( + `\n💾 Writing updated lockfile with ${addedCount} new entries...`, + ); + fs.writeFileSync( + lockPath, + JSON.stringify(lockfile, null, 2) + "\n", + "utf8", + ); + console.log("\n✅ Done! Verify with: git diff package-lock.json"); + console.log( + "Expected: new platform entries added without any version changes\n", + ); + } else { + console.error("\n❌ No packages were added. Check errors above.\n"); + process.exit(1); + } +} + +// --- Helpers --- + +function getPackageNameFromPath(pkgPath) { + // Fix: Use lastIndexOf to catch "node_modules/" at start of string or middle + const index = pkgPath.lastIndexOf("node_modules/"); + if (index !== -1) { + return pkgPath.substring(index + 13); // 13 is length of "node_modules/" + } + return pkgPath; +} + +function traceToWorkspace(startPath, parentMap) { + const queue = [{ path: startPath, child: startPath }]; + const visited = new Set(); + + while (queue.length > 0) { + const { path: current, child } = queue.shift(); + if (visited.has(current)) continue; + visited.add(current); + + // If current path doesn't contain "node_modules", it's a workspace path (e.g. "client") + // OR if it is the explicit root string "" + const isWorkspace = !current.includes("node_modules") || current === ""; + + if (isWorkspace) { + return { + workspace: current, + directDependency: child, + }; + } + + const parents = parentMap.get(current); + if (parents) { + for (const parent of parents) { + // We track 'child' so we know which dependency connects to the workspace + queue.push({ path: parent, child: current }); + } + } + } + return null; +} + +main(); From f495e9b67d6e840aaed000eacdd0262147bd2fea Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Thu, 4 Dec 2025 16:55:45 -0800 Subject: [PATCH 12/18] [Opt] Prefer npm >= 11.3.0, imposing node ^20.17 and >= 22.9.0 From npm/cli: $ git log -1 -p -G'"node":' -U0 --format="" v11.3.0 -- package.json | grep node - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 88023563d..821f56fbc 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,8 @@ "get-intrinsic": "1.3.0" }, "engines": { - "node": ">=22.7.5" + "node": "^20.17.0 || >=22.9.0", + "npm": ">=11.3.0" }, "lint-staged": { "**/*.{js,ts,jsx,tsx,json,md}": [ From 46e0d71581b56d97e918083a73f755cd8a21abda Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Thu, 4 Dec 2025 17:33:15 -0800 Subject: [PATCH 13/18] [Opt] Husky pre-commit tests npm and lockfile --- .husky/pre-commit | 1 + .husky/scripts/validate-lockfile.sh | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100755 .husky/scripts/validate-lockfile.sh diff --git a/.husky/pre-commit b/.husky/pre-commit index 0a3afdcb6..ff2270db4 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,3 @@ npx lint-staged git update-index --again +.husky/scripts/validate-lockfile.sh diff --git a/.husky/scripts/validate-lockfile.sh b/.husky/scripts/validate-lockfile.sh new file mode 100755 index 000000000..ce0893a0a --- /dev/null +++ b/.husky/scripts/validate-lockfile.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# Only run if package-lock.json is being committed +if ! git diff --cached --name-only | grep -q "^package-lock.json$"; then + exit 0 +fi + +# Check npm version +NPM_VERSION=$(npm --version) +NPM_MAJOR=$(echo "$NPM_VERSION" | cut -d. -f1) +NPM_MINOR=$(echo "$NPM_VERSION" | cut -d. -f2) + +if [ "$NPM_MAJOR" -lt 11 ] || ([ "$NPM_MAJOR" -eq 11 ] && [ "$NPM_MINOR" -lt 3 ]); then + echo "" + echo "⚠️ You are using npm $NPM_VERSION" + echo "npm >= 11.3.0 is recommended to avoid lockfile issues." + echo "See: https://github.com/npm/cli/issues/4828" + echo "" +fi + +# Validate lockfile +node scripts/validate-platform-dependencies.js || { + echo "" + echo "❌ package-lock.json validation failed" + echo "Run with --add-missing to fix, or upgrade to npm >= 11.3.0 and regenerate." + exit 1 +} From b4f53f749ce62a56d38d723f80a20e3c49659a9f Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Thu, 4 Dec 2025 18:10:25 -0800 Subject: [PATCH 14/18] Build matrix: Node 20, 22, 24 on Linux and Windows --- .github/workflows/build.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 723e4371c..4116bc4f2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,14 +12,19 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + node-version: [20, 22, 24] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version-file: package.json + node-version: ${{ matrix.node-version }} cache: npm - run: node scripts/validate-platform-dependencies.js From 98988b8a167bbef01a99b5d78d865b3b44c375f9 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Thu, 4 Dec 2025 19:32:16 -0800 Subject: [PATCH 15/18] Do not convert LF to CRLF on checkout, for prettier on Windows Since prettier lacks CRLF configuration, presumably Windows contributors are configuring EOL using git-config. Using `gitattributes` would be more user-friendly for those not knowing git configuration details, and eliminate this config hidden in CI. --- .github/workflows/build.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4116bc4f2..962b453c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,6 +20,13 @@ jobs: node-version: [20, 22, 24] steps: + # Prevent LF→CRLF conversion on Windows checkout (causes prettier to warn). + # Linux and macOS are false by default. TODO: gitattributes + - name: Set git to use LF + run: | + git config --global core.autocrlf false + git config --global core.eol lf + - uses: actions/checkout@v4 - uses: actions/setup-node@v4 From 6ee637ba663defede505ad9795efdeea75903995 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Wed, 3 Dec 2025 20:14:03 -0800 Subject: [PATCH 16/18] Enable manual Run in GitHub UI --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 962b453c9..bfe13ab93 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,6 +10,9 @@ on: # Allow `uses: ...` in other workflows, used by `release.yml` workflow_call: + # Allow users with repo write to run the workflow from the GitHub Actions UI + workflow_dispatch: + jobs: build: runs-on: ${{ matrix.os }} From 14713982f61532cf06c405ca8de81004cc54f137 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Fri, 5 Dec 2025 13:01:28 -0800 Subject: [PATCH 17/18] Update to `setup-node` v6 action --- .github/workflows/build.yml | 6 +++--- .github/workflows/cli_tests.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bfe13ab93..6ef7da9f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: npm @@ -65,7 +65,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version-file: package.json # Omit cache to test with freshly-resolved dependencies @@ -98,7 +98,7 @@ jobs: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 id: setup_node with: node-version-file: package.json diff --git a/.github/workflows/cli_tests.yml b/.github/workflows/cli_tests.yml index 8bd3bb8ec..c978fdbd5 100644 --- a/.github/workflows/cli_tests.yml +++ b/.github/workflows/cli_tests.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: package.json cache: npm diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5c2332b2e..2f2c79888 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version-file: package.json cache: npm From be810fd4f9ca370f56cec29b915b1e1e5a78a745 Mon Sep 17 00:00:00 2001 From: Richard Michael Date: Fri, 5 Dec 2025 13:06:06 -0800 Subject: [PATCH 18/18] Update to `checkout` v6 action GitHub Runner version >= 2.239.0 is required. Workflow `Set up job` runs do indicate that runner version is used: > Current runner version: '2.329.0' --- .github/workflows/build.yml | 6 +++--- .github/workflows/claude.yml | 2 +- .github/workflows/cli_tests.yml | 2 +- .github/workflows/release.yml | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6ef7da9f4..3528137d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: git config --global core.autocrlf false git config --global core.eol lf - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: @@ -63,7 +63,7 @@ jobs: continue-on-error: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: @@ -96,7 +96,7 @@ jobs: sudo apt-get update sudo apt-get install -y libwoff1 - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 id: setup_node diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 2910e4f31..896adaf33 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -28,7 +28,7 @@ jobs: actions: read steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 1 diff --git a/.github/workflows/cli_tests.yml b/.github/workflows/cli_tests.yml index c978fdbd5..2d321c3ad 100644 --- a/.github/workflows/cli_tests.yml +++ b/.github/workflows/cli_tests.yml @@ -15,7 +15,7 @@ jobs: run: working-directory: ./cli steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f2c79888..63a3af157 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: @@ -46,7 +46,7 @@ jobs: id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Log in to the Container registry uses: docker/login-action@v3