VK
Vadim Kononov

Solutions Architect

Preventing Unsafe Package Upgrades in package.json

Preventing Unsafe Package Upgrades in package.json

πŸ“…
nodejsnpmyarnpackage.json

Some packages should not be updated past a certain versionβ€”whether due to breaking changes, compatibility constraints, or known bugs in newer releases. This script helps prevent unsupported upgrades and gives developers clear reasons why certain versions are off limits.

To prevent that, we use a simple script in scripts/preinstall.js that enforces maximum allowed versions for specified packages. It's designed to fail fast, explain the problem clearly, and show developers how to fix it.

Prerequisites

Before using the script, install the required dependency:

yarn add --dev semver

Before proceeding, also wire up the script in your package.json like this:

"scripts": {
  "preinstall": "node ./scripts/preinstall.js"
}

The Script

πŸ’‘
Package limitations are defined in versionConstraints.
const fs = require('fs');
let semver;

try {
  semver = require('semver');
} catch (err) {
  console.warn(`
⚠️  'semver' module not found. Skipping version checks.
    Run 'yarn install' again after dependencies are installed.
`);
  process.exit(0);
}

const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));

// Define your constraints here
const versionConstraints = {
  react: {maxVersion: '18.3.1', reason: 'react-rails is not yet compatible with React 19+'},
  'react-dom': {maxVersion: '18.3.1', reason: 'react-rails is not yet compatible with React 19+'}
};

const errors = [];
const fixes = [];

for (const [pkgName, constraint] of Object.entries(versionConstraints)) {
  const declaredVersion = pkg.dependencies?.[pkgName] || pkg.devDependencies?.[pkgName];

  if (declaredVersion) {
    let minDeclared;

    try {
      minDeclared = semver.minVersion(declaredVersion);
    }
    catch (e) {
      errors.push(`${pkgName}@${declaredVersion} may exceed allowed version ${constraint.maxVersion} (${constraint.reason})`);
      fixes.push(`${pkgName}@~${constraint.maxVersion}`);
      continue;
    }

    if (!minDeclared) {
      errors.push(`${pkgName}@${declaredVersion} may exceed allowed version ${constraint.maxVersion} (${constraint.reason})`);
      fixes.push(`${pkgName}@~${constraint.maxVersion}`);
    }
    else if (semver.gt(minDeclared, constraint.maxVersion)) {
      errors.push(`${pkgName}@${declaredVersion} exceeds allowed version ${constraint.maxVersion} (${constraint.reason})`);
      fixes.push(`${pkgName}@~${constraint.maxVersion}`);
    }
  }
}

if (errors.length > 0) {
  console.error(`
🚫 One or more packages exceed allowed versions. Please fix the following:
   - ${errors.join('\n   - ')}`);

  if (fixes.length > 0) {
    console.error(`
πŸ’‘ To fix, run:
   > yarn add ${fixes.join(' ')} --ignore-scripts
`);
  }

  process.exit(1);
}

Example Output

If a developer changes package.json to include:

"react": "^19.1.0"

They'll get:

🚫 One or more packages exceed allowed versions. Please fix the following:
   - react@^19.1.0 exceeds allowed version 18.3.1 (react-rails is not yet compatible with React 19+)

πŸ’‘ To fix, run:
   > yarn add react@~18.3.1 --ignore-scripts

If an invalid version like "latest" is used:

🚫 One or more packages exceed allowed versions. Please fix the following:
   - react@latest may exceed allowed version 18.3.1 (react-rails is not yet compatible with React 19+)

πŸ’‘ To fix, run:
   > yarn add react@~18.3.1 --ignore-scripts

Features

  • βœ… Enforces a max version cap on any package

  • βœ… Covers both dependencies and devDependencies

  • βœ… Handles common semver ranges like ^, ~, etc.

  • βœ… Detects invalid version formats like latest, file:, link:

  • βœ… Suggests copy-pasteable fix commands scoped only to the offending packages

Limitations

  • ❌ yarn upgrade --latest still updates the lockfile and breaks constraints β€” this script catches it after the fact.

  • ❌ Does not check transitive dependencies (e.g., indirect React installations)

  • ❌ Ignores peer and optional dependencies

  • ❌ Checks declared versions only (not actual installs in node_modules)