const semver = require("semver"); const { fetch } = require("undici"); const fs = require("fs"); const GIT_HASH_RE = /^[0-9a-fA-F]{40}$/; /** * @typedef VersionRange * @property {string} range * @property {boolean} isCanary */ /** * @typedef Version * @property {string} version * @property {boolean} isCanary */ /** * Parses the version from the user into a structure. * * @param {string | undefined} version * @returns {VersionRange | null} */ function parseVersionRange(version) { version = String(version) || "1.x"; version = version.trim(); if (version == "canary") { return { range: "latest", isCanary: true }; } if (version == "latest") { return { range: "latest", isCanary: false }; } if (GIT_HASH_RE.test(version)) { return { range: version, isCanary: true }; } const range = semver.validRange(version); if (range !== null) { return { range, isCanary: false }; } return null; } /** * Parses the version from the version file * * @param {string} versionFilePath * @returns {string | undefined} */ function getDenoVersionFromFile(versionFilePath) { if (!fs.existsSync(versionFilePath)) { throw new Error( `The specified node version file at: ${versionFilePath} does not exist`, ); } const contents = fs.readFileSync(versionFilePath, "utf8"); // .tool-versions typically looks like // ``` // ruby 2.6.5 // deno 1.43.1 // node 20.0.0 // ``` // This parses the version of Deno from the file const denoVersionInToolVersions = contents.match( /^deno\s+v?(?[^\s]+)$/m, ); return denoVersionInToolVersions?.groups?.version || contents.trim(); } /** * @param {VersionRange} range * @returns {Promise} */ async function resolveVersion({ range, isCanary }) { if (isCanary) { if (range === "latest") { const res = await fetchWithRetries( "https://dl.deno.land/canary-latest.txt", ); if (res.status !== 200) { throw new Error( "Failed to fetch canary version info from dl.deno.land. Please try again later.", ); } const version = (await res.text()).trim(); return { version, isCanary: true }; } return { version: range, isCanary: true }; } if (range === "latest") { const res = await fetchWithRetries( "https://dl.deno.land/release-latest.txt", ); if (res.status !== 200) { throw new Error( "Failed to fetch release version info from dl.deno.land. Please try again later.", ); } let version = (await res.text()).trim(); version = semver.clean(version); if (version === null) { return null; } return { version, isCanary: false }; } const res = await fetchWithRetries("https://deno.com/versions.json"); if (res.status !== 200) { throw new Error( "Failed to fetch stable version info from deno.com/versions.json. Please try again later.", ); } const versionJson = await res.json(); if (!("cli" in versionJson)) { throw new Error("Fetched stable version info is invalid."); } /** @type {string[]} */ const versions = versionJson.cli; if (!Array.isArray(versions)) { throw new Error("Fetched stable version info is invalid."); } let version = semver.maxSatisfying(versions, range); if (version === null) { return null; } version = semver.clean(version); if (version === null) { return null; } return { version, isCanary: false }; } /** @param {string} url */ async function fetchWithRetries(url, maxRetries = 5) { let sleepMs = 250; let iterationCount = 0; while (true) { iterationCount++; try { const res = await fetch(url); if (res.status === 200 || iterationCount > maxRetries) { return res; } } catch (err) { if (iterationCount > maxRetries) { throw err; } } console.warn(`Failed fetching. Retrying in ${sleepMs}ms...`); await new Promise((resolve) => setTimeout(resolve, sleepMs)); sleepMs = Math.min(sleepMs * 2, 10_000); } } module.exports = { parseVersionRange, resolveVersion, getDenoVersionFromFile, };