Created
April 29, 2025 09:31
-
-
Save kwikwag/18a81bde8c401acd1563231a7143d0ee to your computer and use it in GitHub Desktop.
A script that ensures your project runs with a Node.js version that satisfies the `engines.node` field in your `package.json`, using either NVM or the system-installed Node.js.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
# `node-engine-strict.js` | |
A script that ensures your project runs with a Node.js version that satisfies | |
the `engines.node` field in your `package.json`. | |
It works by checking for a compatible version installed via **NVM** (Node | |
Version Manager), and will fall back to the system-installed Node.js if it's | |
compatible. If neither is suitable, the script exits with an error. | |
| NVM Installed | Matching NVM Version | Compatible System Node.js | Action | | |
|---------------|----------------------|-----------------------------|--------| | |
| ✅ | ✅ | N/A | Use NVM version | | |
| ✅ | ❌ | ✅ | Use system node | | |
| ✅ | ❌ | ❌ | Fail | | |
| ❌ | N/A | ✅ | Use system node | | |
| ❌ | N/A | ❌ | Fail | | |
## Usage: | |
In `package.json`, prepend your commands with `node <this-script>`: | |
```json | |
{ | |
"scripts": { | |
"start": "node node-engine-strict.js node your-app.js" | |
} | |
} | |
``` | |
*/ | |
/* | |
Copyright 2025 [email protected] | |
Permission is hereby granted, free of charge, to any person obtaining a copy of | |
this software and associated documentation files (the “Software”), to deal in | |
the Software without restriction, including without limitation the rights to | |
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | |
of the Software, and to permit persons to whom the Software is furnished to do | |
so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
SOFTWARE. | |
*/ | |
var fs = require('fs'); | |
var spawn = require('child_process').spawn; | |
var path = require('path'); | |
// Helper to log errors and exit | |
function fail(msg) { | |
console.error('Error:', msg); | |
process.exit(1); | |
} | |
// Try to load semver, or exit with instruction | |
var semver; | |
try { | |
semver = require('semver'); | |
} catch (e) { | |
fail('Error: "semver" module not found. Please run "npm install semver --save-dev" first.'); | |
} | |
// 1. Read package.json (from current working directory) | |
var packageJsonPath = path.resolve(process.cwd(), 'package.json'); | |
if (!fs.existsSync(packageJsonPath)) { | |
fail('package.json not found.'); | |
} | |
var packageJson; | |
try { | |
packageJson = require(packageJsonPath); | |
} catch (e) { | |
fail('Failed to load package.json: ' + e.message); | |
} | |
// 2. Get engines.node | |
var enginesNode = packageJson.engines && packageJson.engines.node; | |
if (!enginesNode) { | |
fail('"engines.node" is not specified in package.json.'); | |
} | |
// 3. Detect NVM and list installed versions | |
var homeDir = process.env.HOME || process.env.USERPROFILE; | |
var nvmDir = process.env.NVM_DIR || (homeDir ? path.join(homeDir, '.nvm') : null); | |
var versionsDir = nvmDir ? path.join(nvmDir, 'versions', 'node') : null; | |
var nvmAvailable = versionsDir && fs.existsSync(versionsDir); | |
var bestVersion = null; | |
var useSystemNode = false; | |
if (nvmAvailable) { | |
var installedVersions = fs.readdirSync(versionsDir).filter(function (v) { | |
return v[0] !== '.'; | |
}).map(function (v) { | |
return v.replace(/^v/, ''); | |
}); | |
for (var i = 0; i < installedVersions.length; i++) { | |
var v = installedVersions[i]; | |
if (semver.satisfies(v, enginesNode)) { | |
if (!bestVersion || semver.gt(v, bestVersion)) { | |
bestVersion = v; | |
} | |
} | |
} | |
} | |
if (!bestVersion) { | |
// No matching NVM version, try system Node.js | |
var systemVersion = process.version.replace(/^v/, ''); | |
if (semver.satisfies(systemVersion, enginesNode)) { | |
bestVersion = systemVersion; | |
useSystemNode = true; | |
if (nvmAvailable) { | |
console.warn('Warning: No matching Node.js version found in NVM. Falling back to system Node.js (v' + bestVersion + ')'); | |
} | |
else { | |
console.log('Using system Node.js version v' + bestVersion + ' (NVM not available)'); | |
} | |
} else { | |
fail('No NVM version satisfies ' + enginesNode + ', and system Node.js version (v' + systemVersion + ') is also incompatible.'); | |
} | |
} | |
// 4. Get the user command | |
var userCommand = process.argv.slice(2).join(' '); | |
if (!userCommand) { | |
fail('No command provided to execute.'); | |
} | |
// 5. Construct the shell command | |
var shellCommand; | |
if (!useSystemNode && nvmAvailable) { | |
shellCommand = '' | |
+ '. "' + path.join(nvmDir, 'nvm.sh') + '"' | |
+ ' && nvm use ' + bestVersion | |
+ ' && ' + userCommand; | |
} else { | |
shellCommand = userCommand; | |
} | |
// 6. Run the command | |
var child = spawn('bash', ['-c', shellCommand], { | |
stdio: 'inherit', | |
env: process.env | |
}); | |
child.on('exit', function (code) { | |
process.exit(code); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
N.B. Written with the assistance of ChatGPT. Meant to be as backwards-compatible as possible, so it wouldn't matter which version of Node.js the script itself it running on (tested with v6, hopefully that's early enough).