@azure/static-web-apps-cli
Version:
Azure Static Web Apps CLI
264 lines • 10.3 kB
JavaScript
import os from "node:os";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { exec } from "node:child_process";
import { promisify } from "node:util";
import { PassThrough } from "node:stream";
import crypto from "node:crypto";
import { fetchWithProxy as fetch } from "./utils/fetch-proxy.js";
import AdmZip from "adm-zip";
import cliProgress from "cli-progress";
import { logger } from "./utils/logger.js";
const RELEASES_FEED_URL = "https://functionscdn.azureedge.net/public/cli-feed-v4.json";
const DEFAULT_FUNC_BINARY = "func";
const VERSION_FILE = ".release-version";
const CORE_TOOLS_FOLDER = ".swa/core-tools";
function getMajorVersion(version) {
return Number(version.split(".")[0]);
}
export function getNodeMajorVersion() {
return getMajorVersion(process.versions.node);
}
function getCoreToolsDownloadFolder() {
return path.join(os.homedir(), CORE_TOOLS_FOLDER);
}
function getCoreToolsFolder(version) {
const downloadFolder = getCoreToolsDownloadFolder();
return path.join(downloadFolder, `v${version}`);
}
function getCoreToolBinaryPath(version) {
const folder = getCoreToolsFolder(version);
return path.resolve(path.join(folder, "func"));
}
export function isCoreToolsVersionCompatible(coreToolsVersion, nodeVersion) {
// Runtime support reference: https://docs.microsoft.com/azure/azure-functions/functions-versions?pivots=programming-language-javascript#languages
switch (coreToolsVersion) {
case 4:
return nodeVersion >= 18 && nodeVersion <= 22;
case 3:
return nodeVersion >= 14 && nodeVersion <= 20;
case 2:
return nodeVersion >= 10 && nodeVersion <= 14;
default:
return false;
}
}
export function detectTargetCoreToolsVersion(nodeVersion) {
// Pick the highest version that is compatible with the specified Node version
if (nodeVersion >= 18 && nodeVersion <= 22)
return 4;
if (nodeVersion >= 14 && nodeVersion < 20)
return 3;
if (nodeVersion >= 10 && nodeVersion < 14)
return 2;
// Fallback to the latest version for Unsupported Node version
return 4;
}
// Cache the results of getInstalledSystemCoreToolsVersion
// This supports the testing inside of func-core-tools.spec.ts
let installedSystemCoreToolsVersionIsCached = false;
let cachedSystemCoreToolsVersion = undefined;
export function setCachedInstalledSystemCoreToolsVersion(version) {
cachedSystemCoreToolsVersion = version;
installedSystemCoreToolsVersionIsCached = true;
}
export function resetCachedInstalledSystemCoreToolsVersion() {
cachedSystemCoreToolsVersion = undefined;
installedSystemCoreToolsVersionIsCached = false;
}
async function getInstalledSystemCoreToolsVersion() {
if (installedSystemCoreToolsVersionIsCached) {
return cachedSystemCoreToolsVersion;
}
try {
const { stdout: version } = await promisify(exec)(`${DEFAULT_FUNC_BINARY} --version`);
return getMajorVersion(version);
}
catch {
return undefined;
}
}
function getDownloadedCoreToolsVersion(targetVersion) {
const folder = getCoreToolsFolder(targetVersion);
if (!fs.existsSync(folder)) {
return undefined;
}
const versionFile = path.join(folder, VERSION_FILE);
if (!fs.existsSync(versionFile)) {
// Cannot detect downloaded version, in doubt cleanup the folder
removeDownloadedCoreTools(targetVersion);
return undefined;
}
return fs.readFileSync(versionFile, "utf8");
}
function getPlatform() {
switch (os.platform()) {
case "win32":
return "Windows";
case "darwin":
return "MacOS";
case "linux":
return "Linux";
default:
throw new Error(`Unsupported platform: ${os.platform()}`);
}
}
export async function getLatestCoreToolsRelease(targetVersion) {
try {
const response = await fetch(RELEASES_FEED_URL);
const feed = (await response.json());
const platform = getPlatform();
const matchingVersions = Object.keys(feed.releases)
.reverse() // JSON has newest versions first; we want the latest first; potential improvement: sort by semver
.filter((version) => version.match(/^\d+\.\d+\.\d+$/) && // only stable versions
version.startsWith(`${targetVersion}.`));
for (const version of matchingVersions) {
const matchingDistribution = feed.releases[version].coreTools?.find((dist) => dist.OS === platform && // Distribution for matching platform
dist.size === "full");
if (matchingDistribution) {
return {
version,
url: matchingDistribution.downloadLink,
sha2: matchingDistribution.sha2,
};
}
}
throw new Error(`Cannot find download package for ${platform}`);
}
catch (error) {
throw new Error(`Error fetching Function Core Tools releases: ${error.message}`);
}
}
async function downloadAndUnzipPackage(release, dest) {
const progressBar = new cliProgress.Bar({
format: "{bar} {percentage}% | ETA: {eta}s",
}, cliProgress.Presets.shades_classic);
try {
const response = await fetch(release.url);
const totalSize = Number(response.headers.get("content-length"));
let downloadedSize = 0;
let since = Date.now();
progressBar.start(totalSize, downloadedSize);
const bodyStream1 = response?.body?.pipe(new PassThrough());
const bodyStream2 = response?.body?.pipe(new PassThrough());
bodyStream2?.on("data", (chunk) => {
downloadedSize += chunk.length;
const now = Date.now();
if (now - since > 100) {
progressBar.update(downloadedSize);
since = now;
}
});
const downloadFilename = release.url.substring(release.url.lastIndexOf("/") + 1);
const outputFile = path.join(dest, downloadFilename);
const writableStream = fs.createWriteStream(outputFile);
bodyStream2?.pipe(writableStream);
const unzipPromise = new Promise((resolve, reject) => {
const timeout = 30;
let timer = setTimeout(() => {
if (downloadedSize === 0) {
writableStream.close();
reject(new Error(`Download failed after ${timeout} seconds. Please check your network or retry.`));
}
}, timeout * 1000);
writableStream.on("finish", () => {
clearTimeout(timer);
resolve(outputFile);
});
writableStream.on("error", (err) => {
reject(err);
});
})
.then(() => {
const zip = new AdmZip(outputFile);
zip.extractAllTo(dest, true);
fs.unlinkSync(outputFile);
})
.catch((err) => {
throw new Error(`Extract failed: ${err.message}`);
});
const hash = await new Promise((resolve) => {
const hash = crypto.createHash("sha256");
hash.setEncoding("hex");
bodyStream1?.on("end", () => {
hash.end();
resolve(hash.read());
});
bodyStream1?.pipe(hash);
});
await unzipPromise;
if (hash !== release.sha2) {
throw new Error(`Downloaded Core Tools SHA2 mismatch: expected ${hash}, got ${release.sha2}`);
}
}
finally {
progressBar.stop();
}
}
export async function downloadCoreTools(version) {
const dest = getCoreToolsFolder(version);
const release = await getLatestCoreToolsRelease(version);
// Make sure we start from a clean folder
if (fs.existsSync(dest)) {
removeDownloadedCoreTools(version);
}
else {
fs.mkdirSync(dest, { recursive: true });
}
try {
await downloadAndUnzipPackage(release, dest);
}
catch (error) {
// Clean up the folder if the download failed
removeDownloadedCoreTools(version);
throw error;
}
// Fix permissions on MacOS/Linux
if (os.platform() === "linux" || os.platform() === "darwin") {
fs.chmodSync(path.join(dest, "func"), 0o755);
fs.chmodSync(path.join(dest, "gozip"), 0o755);
}
fs.writeFileSync(path.join(dest, VERSION_FILE), release.version);
return release.version;
}
export async function getCoreToolsBinary() {
const nodeVersion = getNodeMajorVersion();
const systemVersion = await getInstalledSystemCoreToolsVersion();
if (systemVersion && isCoreToolsVersionCompatible(systemVersion, nodeVersion)) {
return DEFAULT_FUNC_BINARY;
}
const targetVersion = detectTargetCoreToolsVersion(nodeVersion);
const downloadedVersion = getDownloadedCoreToolsVersion(targetVersion);
if (downloadedVersion) {
// Should we check for newer versions here?
return getCoreToolBinaryPath(targetVersion);
}
if (systemVersion && systemVersion !== targetVersion) {
logger.warn(`Functions Core Tools version mismatch. Detected v${systemVersion} but requires v${targetVersion}`);
logger.warn(`Downloading v${targetVersion}...`);
}
if (!systemVersion) {
logger.warn(`Functions Core Tools not detected. Downloading v${targetVersion}...`);
}
try {
await downloadCoreTools(targetVersion);
logger.log(`\nDownloaded Function Core Tools successfully`);
return getCoreToolBinaryPath(targetVersion);
}
catch (error) {
logger.error(`Failed to download Functions Core Tools v${targetVersion}.`);
logger.error(error);
console.log(error);
return undefined;
}
}
export function removeDownloadedCoreTools(version) {
// If not specified, remove all versions
const folder = version ? getCoreToolsFolder(version) : getCoreToolsDownloadFolder();
if (fs.existsSync(folder)) {
const rm = fs.rmSync ? fs.rmSync : fs.rmdirSync;
rm(folder, { recursive: true });
}
}
//# sourceMappingURL=func-core-tools.js.map