feat(platform): prefer stable Node.js versions over pre-releases

- Add PRE_RELEASE_PATTERN to identify beta, rc, alpha, nightly, canary, dev, pre versions
- Modify findNodeFromVersionManager to try stable versions first
- Pre-release versions are used as fallback if no stable version found
- Add tests for pre-release detection and version prioritization

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kacper
2025-12-21 15:28:13 +01:00
parent 9f97426859
commit 41ea6f78eb
2 changed files with 66 additions and 12 deletions

View File

@@ -10,13 +10,12 @@ import fs from 'fs';
import path from 'path';
import os from 'os';
/**
* Pattern to match version directories (e.g., "v18.17.0", "18.17.0", "v18")
* Intentionally permissive to match pre-release versions (v18.17.0-beta, v18.17.0-rc1)
* since localeCompare with numeric:true handles sorting correctly
*/
/** Pattern to match version directories (e.g., "v18.17.0", "18.17.0", "v18") */
const VERSION_DIR_PATTERN = /^v?\d+/;
/** Pattern to identify pre-release versions (beta, rc, alpha, nightly, canary) */
const PRE_RELEASE_PATTERN = /-(beta|rc|alpha|nightly|canary|dev|pre)/i;
/** Result of finding Node.js executable */
export interface NodeFinderResult {
/** Path to the Node.js executable */
@@ -65,11 +64,8 @@ function isExecutable(filePath: string): boolean {
/**
* Find Node.js executable from version manager directories (NVM, fnm)
* Uses semantic version sorting to prefer the latest version
*
* Note: Version sorting uses localeCompare with numeric:true which handles most cases
* correctly (e.g., v18.17.0 > v18.9.0) but may not perfectly sort pre-release versions
* (e.g., v20.0.0-beta vs v19.9.9). This is acceptable as we prefer the latest stable.
* Uses semantic version sorting to prefer the latest stable version
* Pre-release versions (beta, rc, alpha) are deprioritized but used as fallback
*/
function findNodeFromVersionManager(
basePath: string,
@@ -78,13 +74,18 @@ function findNodeFromVersionManager(
if (!fs.existsSync(basePath)) return null;
try {
const versions = fs
const allVersions = fs
.readdirSync(basePath)
.filter((v) => VERSION_DIR_PATTERN.test(v))
// Semantic version sort - newest first using localeCompare with numeric option
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: 'base' }));
for (const version of versions) {
// Separate stable and pre-release versions, preferring stable
const stableVersions = allVersions.filter((v) => !PRE_RELEASE_PATTERN.test(v));
const preReleaseVersions = allVersions.filter((v) => PRE_RELEASE_PATTERN.test(v));
// Try stable versions first, then fall back to pre-release
for (const version of [...stableVersions, ...preReleaseVersions]) {
const nodePath = path.join(basePath, version, binSubpath);
if (isExecutable(nodePath)) {
return nodePath;

View File

@@ -4,6 +4,59 @@ import path from 'path';
import fs from 'fs';
describe('node-finder', () => {
describe('version sorting and pre-release filtering', () => {
// Test the PRE_RELEASE_PATTERN logic indirectly
const PRE_RELEASE_PATTERN = /-(beta|rc|alpha|nightly|canary|dev|pre)/i;
it('should identify pre-release versions correctly', () => {
const preReleaseVersions = [
'v20.0.0-beta',
'v18.17.0-rc1',
'v19.0.0-alpha',
'v21.0.0-nightly',
'v20.0.0-canary',
'v18.0.0-dev',
'v17.0.0-pre',
];
for (const version of preReleaseVersions) {
expect(PRE_RELEASE_PATTERN.test(version)).toBe(true);
}
});
it('should not match stable versions as pre-release', () => {
const stableVersions = ['v18.17.0', 'v20.10.0', 'v16.20.2', '18.17.0', 'v21.0.0'];
for (const version of stableVersions) {
expect(PRE_RELEASE_PATTERN.test(version)).toBe(false);
}
});
it('should sort versions with numeric comparison', () => {
const versions = ['v18.9.0', 'v18.17.0', 'v20.0.0', 'v8.0.0'];
const sorted = [...versions].sort((a, b) =>
b.localeCompare(a, undefined, { numeric: true, sensitivity: 'base' })
);
expect(sorted).toEqual(['v20.0.0', 'v18.17.0', 'v18.9.0', 'v8.0.0']);
});
it('should prefer stable over pre-release when filtering', () => {
const allVersions = ['v20.0.0-beta', 'v19.9.9', 'v18.17.0', 'v21.0.0-rc1'];
const stableVersions = allVersions.filter((v) => !PRE_RELEASE_PATTERN.test(v));
const preReleaseVersions = allVersions.filter((v) => PRE_RELEASE_PATTERN.test(v));
const prioritized = [...stableVersions, ...preReleaseVersions];
// Stable versions should come first
expect(prioritized[0]).toBe('v19.9.9');
expect(prioritized[1]).toBe('v18.17.0');
// Pre-release versions should come after
expect(prioritized[2]).toBe('v20.0.0-beta');
expect(prioritized[3]).toBe('v21.0.0-rc1');
});
});
describe('findNodeExecutable', () => {
it("should return 'node' with fallback source when skipSearch is true", () => {
const result = findNodeExecutable({ skipSearch: true });