| 1 | import path from "node:path"; |
| 2 | import { errMsg, failure } from "../job.js"; |
| 3 | import { maxSatisfying, parseVer, satisfies, truthyInput } from "../util.js"; |
| 4 | import type { Builtin } from "./index.js"; |
| 5 | import { fetchJson, installTarball, isCached, packageCache, probe, readVersionFile, toolArch, toolDir } from "./tools.js"; |
| 6 | |
| 7 | interface NodeDistEntry { |
| 8 | version: string; // "v22.11.0" |
| 9 | lts: string | false; |
| 10 | files: string[]; |
| 11 | } |
| 12 | |
| 13 | export function nodeMirror(): string { |
| 14 | return (process.env.DIFFINERY_NODE_MIRROR ?? "https://nodejs.org/dist").replace(/\/$/, ""); |
| 15 | } |
| 16 | |
| 17 | /** Resolves a setup-node version spec against nodejs.org's index. */ |
| 18 | export function resolveNodeVersion(index: NodeDistEntry[], spec: string): string | null { |
| 19 | const s = spec.trim().toLowerCase(); |
| 20 | const arch = `linux-${toolArch("node")}`; |
| 21 | const avail = index.filter((e) => e.files.includes(arch) || e.files.length === 0); |
| 22 | if (s.startsWith("lts/")) { |
| 23 | const code = s.slice(4); |
| 24 | const lts = avail.filter((e) => e.lts && (code === "*" || String(e.lts).toLowerCase() === code)); |
| 25 | if (code.startsWith("-")) { |
| 26 | // lts/-1: previous LTS line |
| 27 | const n = Number(code.slice(1)); |
| 28 | const majors = [...new Set(avail.filter((e) => e.lts).map((e) => parseVer(e.version)![0]))].sort((a, b) => b - a); |
| 29 | const major = majors[n]; |
| 30 | return major === undefined ? null : maxSatisfying(avail.filter((e) => e.lts).map((e) => e.version), String(major))?.replace(/^v/, "") ?? null; |
| 31 | } |
| 32 | return maxSatisfying(lts.map((e) => e.version), "*")?.replace(/^v/, "") ?? null; |
| 33 | } |
| 34 | if (s === "latest" || s === "current" || s === "node") return maxSatisfying(avail.map((e) => e.version), "*")?.replace(/^v/, "") ?? null; |
| 35 | return maxSatisfying(avail.map((e) => e.version), s.replace(/^v/, ""))?.replace(/^v/, "") ?? null; |
| 36 | } |
| 37 | |
| 38 | export const setupNode: Builtin = async (c) => { |
| 39 | let spec = c.input("node-version"); |
| 40 | const file = c.input("node-version-file"); |
| 41 | if (!spec && file) spec = await readVersionFile(c, file, "node"); |
| 42 | const installed = (await probe(c, "node", ["--version"]))?.replace(/^v/, "") ?? null; |
| 43 | let version = installed; |
| 44 | |
| 45 | if (spec) { |
| 46 | const s = spec.trim().replace(/^v(?=\d)/, ""); |
| 47 | let ok = false; |
| 48 | if (installed && !truthyInput(c.input("check-latest"), false)) { |
| 49 | if (/^(lts\/|latest|current|node$)/i.test(s)) { |
| 50 | try { |
| 51 | const target = resolveNodeVersion(await fetchJson<NodeDistEntry[]>(`${nodeMirror()}/index.json`), s); |
| 52 | ok = !!target && parseVer(target)![0] === parseVer(installed)![0]; |
| 53 | } catch (e) { |
| 54 | c.warn(`Could not resolve '${s}' (${errMsg(e)}); using installed node ${installed}.`); |
| 55 | ok = true; |
| 56 | } |
| 57 | } else { |
| 58 | const want = parseVer(s); |
| 59 | ok = satisfies(installed, s) || (!!want && /^\d/.test(s) && want[0] === parseVer(installed)![0]); |
| 60 | } |
| 61 | } |
| 62 | if (ok) { |
| 63 | c.log(`Found in cache @ ${installed} (pre-installed node satisfies '${s}')`); |
| 64 | } else { |
| 65 | let index: NodeDistEntry[]; |
| 66 | try { |
| 67 | index = await fetchJson<NodeDistEntry[]>(`${nodeMirror()}/index.json`); |
| 68 | } catch (e) { |
| 69 | throw failure(`Unable to fetch the Node.js version index: ${errMsg(e)}`); |
| 70 | } |
| 71 | const v = resolveNodeVersion(index, s); |
| 72 | if (!v) throw failure(`Unable to find Node version '${s}' for platform linux and architecture ${toolArch("node")}.`); |
| 73 | const dir = toolDir(c, "node", v, toolArch("node")); |
| 74 | if (!isCached(dir)) { |
| 75 | await installTarball(c, `${nodeMirror()}/v${v}/node-v${v}-linux-${toolArch("node")}.tar.gz`, dir); |
| 76 | } else c.log(`Found in cache @ ${dir}`); |
| 77 | c.addPath(path.join(dir, "bin")); |
| 78 | version = v; |
| 79 | } |
| 80 | } else { |
| 81 | c.log(`No node-version specified; using the pre-installed node ${installed ?? "(none)"}.`); |
| 82 | } |
| 83 | if (version) c.setOutput("node-version", `v${version}`); |
| 84 | |
| 85 | const registry = c.input("registry-url"); |
| 86 | if (registry) { |
| 87 | // Minimal setup-node `registry-url` support: write an .npmrc that reads NODE_AUTH_TOKEN. |
| 88 | const { writeFile, mkdir } = await import("node:fs/promises"); |
| 89 | const npmrc = path.join(c.job.tempDir, ".npmrc"); |
| 90 | await mkdir(c.job.tempDir, { recursive: true }); |
| 91 | const url = registry.endsWith("/") ? registry : `${registry}/`; |
| 92 | const scope = c.input("scope"); |
| 93 | const authLine = `${url.replace(/^\w+:/, "")}:_authToken=\${NODE_AUTH_TOKEN}`; |
| 94 | const regLine = scope ? `${scope.startsWith("@") ? scope : "@" + scope}:registry=${url}` : `registry=${url}`; |
| 95 | await writeFile(npmrc, `${authLine}\n${regLine}\n`); |
| 96 | c.exportVariable("NPM_CONFIG_USERCONFIG", npmrc); |
| 97 | c.exportVariable("NODE_AUTH_TOKEN", "XXXXX-XXXXX-XXXXX-XXXXX"); |
| 98 | } |
| 99 | |
| 100 | const pm = c.input("cache").toLowerCase(); |
| 101 | if (pm) { |
| 102 | const env = c.toolEnv(); |
| 103 | let dir: string | null = null; |
| 104 | let locks: string[]; |
| 105 | if (pm === "npm") { |
| 106 | dir = await probe(c, "npm", ["config", "get", "cache"], env); |
| 107 | locks = ["**/package-lock.json", "**/npm-shrinkwrap.json"]; |
| 108 | } else if (pm === "pnpm") { |
| 109 | dir = await probe(c, "pnpm", ["store", "path", "--silent"], env); |
| 110 | locks = ["**/pnpm-lock.yaml"]; |
| 111 | } else if (pm === "yarn") { |
| 112 | const ver = await probe(c, "yarn", ["--version"], env); |
| 113 | dir = ver && parseVer(ver)![0] >= 2 ? await probe(c, "yarn", ["config", "get", "cacheFolder"], env) : await probe(c, "yarn", ["cache", "dir"], env); |
| 114 | locks = ["**/yarn.lock"]; |
| 115 | } else { |
| 116 | throw failure(`Caching for '${pm}' is not supported`); |
| 117 | } |
| 118 | await packageCache(c, { |
| 119 | prefix: `node-cache-Linux-${toolArch("node")}-${pm}`, |
| 120 | paths: dir ? [dir] : [], |
| 121 | lockPatterns: locks, |
| 122 | what: pm, |
| 123 | }); |
| 124 | } |
| 125 | }; |