live-plugin-manager
Version:
Install and uninstall any node package at runtime from npm registry
181 lines (150 loc) • 4.65 kB
text/typescript
import urlJoin = require("url-join");
import * as path from "path";
import * as fs from "./fileSystem";
import { downloadTarball, extractTarball } from "./tarballUtils";
import * as semVer from "semver";
import * as httpUtils from "./httpUtils";
import { PackageInfo } from "./PackageInfo";
import Debug from "debug";
const debug = Debug("live-plugin-manager.NpmRegistryClient");
export class NpmRegistryClient {
defaultHeaders: httpUtils.Headers;
constructor(private readonly npmUrl: string, config: NpmRegistryConfig) {
const staticHeaders = {
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
"accept-encoding": "gzip",
"accept": "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*",
"user-agent": config.userAgent || "live-plugin-manager"
};
const authHeader = createAuthHeader(config.auth);
this.defaultHeaders = {...staticHeaders, ...authHeader};
}
async get(name: string, versionOrTag: string | null = "latest"): Promise<PackageInfo> {
debug(`Getting npm info for ${name}:${versionOrTag}...`);
if (typeof versionOrTag !== "string") {
versionOrTag = "";
}
if (typeof name !== "string") {
throw new Error("Invalid package name");
}
const data = await this.getNpmData(name);
versionOrTag = versionOrTag.trim();
// check if there is a tag (es. latest)
const distTags = data["dist-tags"];
let version = distTags && distTags[versionOrTag];
if (!version) {
version = semVer.clean(versionOrTag) || versionOrTag;
}
// find correct version
let pInfo = data.versions[version];
if (!pInfo) {
// find compatible version
for (const pVersion in data.versions) {
if (!data.versions.hasOwnProperty(pVersion)) {
continue;
}
const pVersionInfo = data.versions[pVersion];
if (!semVer.satisfies(pVersionInfo.version, version)) {
continue;
}
if (!pInfo || semVer.gt(pVersionInfo.version, pInfo.version)) {
pInfo = pVersionInfo;
}
}
}
if (!pInfo) {
throw new Error(`Version '${versionOrTag} not found`);
}
return {
dist: pInfo.dist,
name: pInfo.name,
version: pInfo.version
};
}
async download(
destinationDirectory: string,
packageInfo: PackageInfo): Promise<string> {
if (!packageInfo.dist || !packageInfo.dist.tarball) {
throw new Error("Invalid dist.tarball property");
}
const tgzFile = await downloadTarball(packageInfo.dist.tarball, this.defaultHeaders);
const pluginDirectory = path.join(destinationDirectory, packageInfo.name);
try {
await extractTarball(tgzFile, pluginDirectory);
} finally {
await fs.remove(tgzFile);
}
return pluginDirectory;
}
private async getNpmData(name: string): Promise<NpmData> {
const regUrl = urlJoin(this.npmUrl, encodeNpmName(name));
const headers = this.defaultHeaders;
try {
const result = await httpUtils.httpJsonGet<NpmData>(regUrl, headers);
if (!result) {
throw new Error("Response is empty");
}
if (!result.versions
|| !result.name) {
throw new Error("Invalid json format");
}
return result;
} catch (err: any) {
if (err.message) {
err.message = `Failed to get package '${name}' ${err.message}`;
}
throw err;
}
}
}
// example: https://registry.npmjs.org/lodash/
// or https://registry.npmjs.org/@types%2Fnode (for scoped)
interface NpmData {
name: string;
"dist-tags"?: {
// "latest": "1.0.0";
[tag: string]: string;
};
versions: {
[version: string]: {
dist: {
tarball: string;
},
name: string,
version: string
}
};
}
function encodeNpmName(name: string) {
return name.replace("/", "%2F");
}
export interface NpmRegistryConfig {
// actually this is used in the params
auth?: NpmRegistryAuthToken | NpmRegistryAuthBasic;
userAgent?: string;
}
export interface NpmRegistryAuthToken {
token: string;
}
export interface NpmRegistryAuthBasic {
username: string;
password: string;
}
function createAuthHeader(auth?: NpmRegistryAuthToken | NpmRegistryAuthBasic): httpUtils.Headers {
if (!auth) {
return {};
}
if (isTokenAuth(auth)) {
return httpUtils.headersBearerAuth(auth.token); // this should be a JWT I think...
} else if (isBasicAuth(auth)) {
return httpUtils.headersBasicAuth(auth.username, auth.password);
} else {
return {};
}
}
function isTokenAuth(arg: NpmRegistryAuthToken | NpmRegistryAuthBasic): arg is NpmRegistryAuthToken {
return (arg as NpmRegistryAuthToken).token !== undefined;
}
function isBasicAuth(arg: NpmRegistryAuthToken | NpmRegistryAuthBasic): arg is NpmRegistryAuthBasic {
return (arg as NpmRegistryAuthBasic).username !== undefined;
}