Skip to content

Instantly share code, notes, and snippets.

@kwikwag
Created April 29, 2025 09:31
Show Gist options
  • Save kwikwag/18a81bde8c401acd1563231a7143d0ee to your computer and use it in GitHub Desktop.
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.
/*
# `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);
});
@kwikwag
Copy link
Author

kwikwag commented Apr 29, 2025

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment