supabase-cli/scripts/postinstall.js

181 lines
5.6 KiB
JavaScript

#!/usr/bin/env node
// Ref 1: https://github.com/sanathkr/go-npm
// Ref 2: https://medium.com/xendit-engineering/how-we-repurposed-npm-to-publish-and-distribute-our-go-binaries-for-internal-cli-23981b80911b
"use strict";
import binLinks from "bin-links";
import { createHash } from "crypto";
import fs from "fs";
import fetch from "node-fetch";
import { Agent } from "https";
import { HttpsProxyAgent } from "https-proxy-agent";
import path from "path";
import { extract } from "tar";
import zlib from "zlib";
// Mapping from Node's `process.arch` to Golang's `$GOARCH`
const ARCH_MAPPING = {
x64: "amd64",
arm64: "arm64",
};
// Mapping between Node's `process.platform` to Golang's
const PLATFORM_MAPPING = {
darwin: "darwin",
linux: "linux",
win32: "windows",
};
const arch = ARCH_MAPPING[process.arch];
const platform = PLATFORM_MAPPING[process.platform];
// TODO: import pkg from "../package.json" assert { type: "json" };
const readPackageJson = async () => {
const contents = await fs.promises.readFile("package.json");
return JSON.parse(contents);
};
// Build the download url from package.json
const getDownloadUrl = (packageJson) => {
const pkgName = packageJson.name;
const version = packageJson.version;
const repo = packageJson.repository;
const url = `https://github.com/${repo}/releases/download/v${version}/${pkgName}_${platform}_${arch}.tar.gz`;
return url;
};
const fetchAndParseCheckSumFile = async (packageJson, agent) => {
const version = packageJson.version;
const pkgName = packageJson.name;
const repo = packageJson.repository;
const checksumFileUrl = `https://github.com/${repo}/releases/download/v${version}/${pkgName}_${version}_checksums.txt`;
// Fetch the checksum file
console.info("Downloading", checksumFileUrl);
const response = await fetch(checksumFileUrl, { agent });
if (response.ok) {
const checkSumContent = await response.text();
const lines = checkSumContent.split("\n");
const checksums = {};
for (const line of lines) {
const [checksum, packageName] = line.split(/\s+/);
checksums[packageName] = checksum;
}
return checksums;
} else {
console.error(
"Could not fetch checksum file",
response.status,
response.statusText
);
}
};
const errGlobal = `Installing Supabase CLI as a global module is not supported.
Please use one of the supported package managers: https://github.com/supabase/cli#install-the-cli
`;
const errChecksum = "Checksum mismatch. Downloaded data might be corrupted.";
const errUnsupported = `Installation is not supported for ${process.platform} ${process.arch}`;
/**
* Reads the configuration from application's package.json,
* downloads the binary from package url and stores at
* ./bin in the package's root.
*
* See: https://docs.npmjs.com/files/package.json#bin
*/
async function main() {
const yarnGlobal = JSON.parse(
process.env.npm_config_argv || "{}"
).original?.includes("global");
if (process.env.npm_config_global || yarnGlobal) {
throw errGlobal;
}
if (!arch || !platform) {
throw errUnsupported;
}
// Read from package.json and prepare for the installation.
const pkg = await readPackageJson();
if (platform === "windows") {
// Update bin path in package.json
pkg.bin[pkg.name] += ".exe";
}
// Prepare the installation path by creating the directory if it doesn't exist.
const binPath = pkg.bin[pkg.name];
const binDir = path.dirname(binPath);
await fs.promises.mkdir(binDir, { recursive: true });
// Create the agent that will be used for all the fetch requests later.
const proxyUrl =
process.env.npm_config_https_proxy ||
process.env.npm_config_http_proxy ||
process.env.npm_config_proxy;
// Keeps the TCP connection alive when sending multiple requests
// Ref: https://github.com/node-fetch/node-fetch/issues/1735
const agent = proxyUrl
? new HttpsProxyAgent(proxyUrl, { keepAlive: true })
: new Agent({ keepAlive: true });
// First, fetch the checksum map.
const checksumMap = await fetchAndParseCheckSumFile(pkg, agent);
// Then, download the binary.
const url = getDownloadUrl(pkg);
console.info("Downloading", url);
const resp = await fetch(url, { agent });
const hash = createHash("sha256");
const pkgNameWithPlatform = `${pkg.name}_${platform}_${arch}.tar.gz`;
// Then, decompress the binary -- we will first Un-GZip, then we will untar.
const ungz = zlib.createGunzip();
const binName = path.basename(binPath);
const untar = extract({ cwd: binDir }, [binName]);
// Update the hash with the binary data as it's being downloaded.
resp.body
.on("data", (chunk) => {
hash.update(chunk);
})
// Pipe the data to the ungz stream.
.pipe(ungz);
// After the ungz stream has ended, verify the checksum.
ungz
.on("end", () => {
const expectedChecksum = checksumMap?.[pkgNameWithPlatform];
// Skip verification if we can't find the file checksum
if (!expectedChecksum) {
console.warn("Skipping checksum verification");
return;
}
const calculatedChecksum = hash.digest("hex");
if (calculatedChecksum !== expectedChecksum) {
throw errChecksum;
}
console.info("Checksum verified.");
})
// Pipe the data to the untar stream.
.pipe(untar);
// Wait for the untar stream to finish.
await new Promise((resolve, reject) => {
untar.on("error", reject);
untar.on("end", () => resolve());
});
// Link the binaries in postinstall to support yarn
await binLinks({
path: path.resolve("."),
pkg: { ...pkg, bin: { [pkg.name]: binPath } },
});
console.info("Installed Supabase CLI successfully");
}
await main();