216 lines
7.8 KiB
JavaScript
216 lines
7.8 KiB
JavaScript
import { readFile, writeFile, rm, mkdir } from 'node:fs/promises';
|
|
import { homedir } from 'node:os';
|
|
import { existsSync, createWriteStream, readdirSync } from 'node:fs';
|
|
import { extract } from 'tar';
|
|
import { resolve, relative, dirname } from 'pathe';
|
|
import { defu } from 'defu';
|
|
import { pipeline } from 'node:stream';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { promisify } from 'node:util';
|
|
import { fetch } from 'node-fetch-native';
|
|
import createHttpsProxyAgent from 'https-proxy-agent';
|
|
|
|
async function download(url, filePath, options = {}) {
|
|
const infoPath = filePath + ".json";
|
|
const info = JSON.parse(await readFile(infoPath, "utf8").catch(() => "{}"));
|
|
const headResponse = await sendFetch(url, { method: "HEAD", headers: options.headers }).catch(() => void 0);
|
|
const etag = headResponse?.headers.get("etag");
|
|
if (info.etag === etag && existsSync(filePath)) {
|
|
return;
|
|
}
|
|
info.etag = etag;
|
|
const response = await sendFetch(url, { headers: options.headers });
|
|
if (response.status >= 400) {
|
|
throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
|
|
}
|
|
const stream = createWriteStream(filePath);
|
|
await promisify(pipeline)(response.body, stream);
|
|
await writeFile(infoPath, JSON.stringify(info), "utf8");
|
|
}
|
|
const inputRegex = /^(?<repo>[\w.-]+\/[\w.-]+)(?<subdir>[^#]+)?(?<ref>#[\w.-]+)?/;
|
|
function parseGitURI(input) {
|
|
const m = input.match(inputRegex)?.groups;
|
|
return {
|
|
repo: m.repo,
|
|
subdir: m.subdir || "/",
|
|
ref: m.ref ? m.ref.slice(1) : "main"
|
|
};
|
|
}
|
|
function debug(...arguments_) {
|
|
if (process.env.DEBUG) {
|
|
console.debug("[giget]", ...arguments_);
|
|
}
|
|
}
|
|
async function sendFetch(url, options) {
|
|
const proxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
|
|
const requestOptions = proxy ? { agent: createHttpsProxyAgent(proxy), ...options } : options;
|
|
return await fetch(url, requestOptions);
|
|
}
|
|
function currentShell() {
|
|
if (process.env.SHELL) {
|
|
return process.env.SHELL;
|
|
}
|
|
if (process.platform === "win32") {
|
|
return "cmd.exe";
|
|
}
|
|
return "/bin/bash";
|
|
}
|
|
function startShell(cwd) {
|
|
cwd = resolve(cwd);
|
|
const shell = currentShell();
|
|
console.info(`(experimental) Opening shell in ${relative(process.cwd(), cwd)}...`);
|
|
spawnSync(shell, [], {
|
|
cwd,
|
|
shell: true,
|
|
stdio: "inherit"
|
|
});
|
|
}
|
|
|
|
const github = (input, options) => {
|
|
const parsed = parseGitURI(input);
|
|
return {
|
|
name: parsed.repo.replace("/", "-"),
|
|
version: parsed.ref,
|
|
subdir: parsed.subdir,
|
|
headers: { Authorization: options.auth ? `Bearer ${options.auth}` : void 0 },
|
|
url: `https://github.com/${parsed.repo}/tree/${parsed.ref}${parsed.subdir}`,
|
|
tar: `https://github.com/${parsed.repo}/archive/${parsed.ref}.tar.gz`
|
|
};
|
|
};
|
|
const gitlab = (input, options) => {
|
|
const parsed = parseGitURI(input);
|
|
return {
|
|
name: parsed.repo.replace("/", "-"),
|
|
version: parsed.ref,
|
|
subdir: parsed.subdir,
|
|
headers: { Authorization: options.auth ? `Bearer ${options.auth}` : void 0 },
|
|
url: `https://gitlab.com/${parsed.repo}/tree/${parsed.ref}${parsed.subdir}`,
|
|
tar: `https://gitlab.com/${parsed.repo}/-/archive/${parsed.ref}.tar.gz`
|
|
};
|
|
};
|
|
const bitbucket = (input, options) => {
|
|
const parsed = parseGitURI(input);
|
|
return {
|
|
name: parsed.repo.replace("/", "-"),
|
|
version: parsed.ref,
|
|
subdir: parsed.subdir,
|
|
headers: { Authorization: options.auth ? `Bearer ${options.auth}` : void 0 },
|
|
url: `https://bitbucket.com/${parsed.repo}/src/${parsed.ref}${parsed.subdir}`,
|
|
tar: `https://bitbucket.org/${parsed.repo}/get/${parsed.ref}.tar.gz`
|
|
};
|
|
};
|
|
const sourcehut = (input, options) => {
|
|
const parsed = parseGitURI(input);
|
|
return {
|
|
name: parsed.repo.replace("/", "-"),
|
|
version: parsed.ref,
|
|
subdir: parsed.subdir,
|
|
headers: { Authorization: options.auth ? `Bearer ${options.auth}` : void 0 },
|
|
url: `https://git.sr.ht/~${parsed.repo}/tree/${parsed.ref}/item${parsed.subdir}`,
|
|
tar: `https://git.sr.ht/~${parsed.repo}/archive/${parsed.ref}.tar.gz`
|
|
};
|
|
};
|
|
const providers = {
|
|
github,
|
|
gh: github,
|
|
gitlab,
|
|
bitbucket,
|
|
sourcehut
|
|
};
|
|
|
|
const DEFAULT_REGISTRY = "https://raw.githubusercontent.com/unjs/giget/main/templates";
|
|
const registryProvider = (registryEndpoint = DEFAULT_REGISTRY) => {
|
|
return async (input) => {
|
|
const start = Date.now();
|
|
const registryURL = `${registryEndpoint}/${input}.json`;
|
|
const result = await sendFetch(registryURL);
|
|
if (result.status >= 400) {
|
|
throw new Error(`Failed to download ${input} template info from ${registryURL}: ${result.status} ${result.statusText}`);
|
|
}
|
|
const info = await result.json();
|
|
if (!info.tar || !info.name) {
|
|
throw new Error(`Invalid template info from ${registryURL}. name or tar fields are missing!`);
|
|
}
|
|
debug(`Fetched ${input} template info from ${registryURL} in ${Date.now() - start}ms`);
|
|
return info;
|
|
};
|
|
};
|
|
|
|
const sourceProtoRe = /^([\w-.]+):/;
|
|
async function downloadTemplate(input, options = {}) {
|
|
options = defu({
|
|
registry: process.env.GIGET_REGISTRY,
|
|
auth: process.env.GIGET_AUTH
|
|
}, options);
|
|
const registry = options.registry !== false ? registryProvider(options.registry) : void 0;
|
|
let providerName = options.provider || (registryProvider ? "registry" : "github");
|
|
let source = input;
|
|
const sourceProvierMatch = input.match(sourceProtoRe);
|
|
if (sourceProvierMatch) {
|
|
providerName = sourceProvierMatch[1];
|
|
source = input.slice(sourceProvierMatch[0].length);
|
|
}
|
|
const provider = options.providers?.[providerName] || providers[providerName] || registry;
|
|
if (!provider) {
|
|
throw new Error(`Unsupported provider: ${providerName}`);
|
|
}
|
|
const template = await Promise.resolve().then(() => provider(source, { auth: options.auth })).catch((error) => {
|
|
throw new Error(`Failed to download template from ${providerName}: ${error.message}`);
|
|
});
|
|
template.name = (template.name || "template").replace(/[^\da-z-]/gi, "-");
|
|
template.defaultDir = (template.defaultDir || template.name).replace(/[^\da-z-]/gi, "-");
|
|
const cwd = resolve(options.cwd || ".");
|
|
const extractPath = resolve(cwd, options.dir || template.defaultDir);
|
|
if (options.forceClean) {
|
|
await rm(extractPath, { recursive: true, force: true });
|
|
}
|
|
if (!options.force && existsSync(extractPath) && readdirSync(extractPath).length > 0) {
|
|
throw new Error(`Destination ${extractPath} already exists.`);
|
|
}
|
|
await mkdir(extractPath, { recursive: true });
|
|
const temporaryDirectory = resolve(homedir(), ".giget", options.provider, template.name);
|
|
const tarPath = resolve(temporaryDirectory, (template.version || template.name) + ".tar.gz");
|
|
if (options.preferOffline && existsSync(tarPath)) {
|
|
options.offline = true;
|
|
}
|
|
if (!options.offline) {
|
|
await mkdir(dirname(tarPath), { recursive: true });
|
|
const s2 = Date.now();
|
|
await download(template.tar, tarPath, { headers: template.headers }).catch((error) => {
|
|
if (!existsSync(tarPath)) {
|
|
throw error;
|
|
}
|
|
debug("Download error. Using cached version:", error);
|
|
options.offline = true;
|
|
});
|
|
debug(`Downloaded ${template.tar} to ${tarPath} in ${Date.now() - s2}ms`);
|
|
}
|
|
if (!existsSync(tarPath)) {
|
|
throw new Error(`Tarball not found: ${tarPath} (offline: ${options.offline})`);
|
|
}
|
|
const s = Date.now();
|
|
const subdir = template.subdir?.replace(/^\//, "") || "";
|
|
await extract({
|
|
file: tarPath,
|
|
cwd: extractPath,
|
|
onentry(entry) {
|
|
entry.path = entry.path.split("/").splice(1).join("/");
|
|
if (subdir) {
|
|
if (entry.path.startsWith(subdir + "/")) {
|
|
entry.path = entry.path.slice(subdir.length);
|
|
} else {
|
|
entry.path = "";
|
|
}
|
|
}
|
|
}
|
|
});
|
|
debug(`Extracted to ${extractPath} in ${Date.now() - s}ms`);
|
|
return {
|
|
...template,
|
|
source,
|
|
dir: extractPath
|
|
};
|
|
}
|
|
|
|
export { downloadTemplate as d, registryProvider as r, startShell as s };
|