-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathnode-version-file.ts
More file actions
141 lines (120 loc) · 4.09 KB
/
Copy pathnode-version-file.ts
File metadata and controls
141 lines (120 loc) · 4.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import { info } from "@actions/core";
import { readFileSync } from "node:fs";
import { basename } from "node:path";
import { getWorkspaceDir, resolvePath } from "./utils.js";
/**
* Resolve a Node.js version from a version file.
*
* Supports: .nvmrc, .node-version, .tool-versions, package.json
*/
export function resolveNodeVersionFile(filePath: string, baseDir?: string): string {
const fullPath = resolvePath(filePath, baseDir || getWorkspaceDir());
let content: string;
try {
content = readFileSync(fullPath, "utf-8");
} catch {
throw new Error(`node-version-file not found: ${fullPath}`);
}
const filename = basename(fullPath);
let version: string | undefined;
if (filename === ".tool-versions") {
version = parseToolVersions(content);
} else if (filename === "package.json") {
version = parsePackageJson(content);
} else {
// .nvmrc, .node-version, or any other plain text file
version = parsePlainVersionFile(content);
}
if (!version) {
throw new Error(`No Node.js version found in ${filePath}`);
}
// Strip leading 'v' prefix (e.g., "v20.11.0" -> "20.11.0")
version = version.replace(/^v/i, "");
info(`Resolved Node.js version '${version}' from ${filePath}`);
return version;
}
/**
* Parse a plain text version file (.nvmrc, .node-version, etc).
* Returns the first non-empty, non-comment line, normalized for vp CLI.
*
* nvm aliases are translated: "node" / "stable" → "latest"
* Inline comments (after #) are stripped.
*/
function parsePlainVersionFile(content: string): string | undefined {
for (const line of content.split("\n")) {
// Strip inline comments
const stripped = line.includes("#") ? line.slice(0, line.indexOf("#")) : line;
const trimmed = stripped.trim();
if (!trimmed) continue;
return normalizeNvmAlias(trimmed);
}
return undefined;
}
function normalizeNvmAlias(version: string): string {
const lower = version.toLowerCase();
if (lower === "node" || lower === "stable") return "latest";
return version;
}
/**
* Parse .tool-versions (asdf format).
* Looks for 'nodejs' or 'node' entries.
* Skips non-version specs (system, ref:*, path:*) and picks the first
* installable version when multiple fallback versions are listed.
*/
function parseToolVersions(content: string): string | undefined {
for (const line of content.split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const [tool, ...versions] = trimmed.split(/\s+/);
if (tool !== "nodejs" && tool !== "node") continue;
// asdf allows multiple fallback versions; pick the first installable one
for (const v of versions) {
if (isAsdfInstallableVersion(v)) return v;
}
}
return undefined;
}
function isAsdfInstallableVersion(version: string): boolean {
return (
!!version && version !== "system" && !version.startsWith("ref:") && !version.startsWith("path:")
);
}
/**
* Parse package.json for Node.js version.
* Priority (matching actions/setup-node):
* 1. devEngines.runtime (name: "node")
* 2. engines.node
*/
function parsePackageJson(content: string): string | undefined {
let pkg: Record<string, unknown>;
try {
pkg = JSON.parse(content) as Record<string, unknown>;
} catch {
throw new Error("Failed to parse package.json: invalid JSON");
}
// Check devEngines.runtime first
const devEngines = pkg.devEngines as Record<string, unknown> | undefined;
if (devEngines?.runtime) {
const version = findNodeRuntime(devEngines.runtime);
if (version) return version;
}
// Fall back to engines.node
const engines = pkg.engines as Record<string, unknown> | undefined;
if (engines?.node && typeof engines.node === "string") {
return engines.node;
}
return undefined;
}
interface RuntimeEntry {
name?: string;
version?: string;
}
function findNodeRuntime(runtime: unknown): string | undefined {
const entries = Array.isArray(runtime) ? runtime : [runtime];
for (const entry of entries as RuntimeEntry[]) {
if (entry?.name === "node" && typeof entry.version === "string") {
return entry.version;
}
}
return undefined;
}