123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- import { readFile, writeFile, mkdir, rm } from 'node:fs/promises';
- import { existsSync, createWriteStream, readdirSync } from 'node:fs';
- import { extract } from 'tar';
- import { resolve, relative, basename, dirname } from 'pathe';
- import { defu } from 'defu';
- import { installDependencies } from 'nypm';
- import { pipeline } from 'node:stream';
- import { spawnSync } from 'node:child_process';
- import { homedir } from 'node:os';
- import { promisify } from 'node:util';
- import { fetch } from 'node-fetch-native/proxy';
- 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;
- }
- if (typeof etag === "string") {
- 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(...args) {
- if (process.env.DEBUG) {
- console.debug("[giget]", ...args);
- }
- }
- async function sendFetch(url, options = {}) {
- if (options.headers?.["sec-fetch-mode"]) {
- options.mode = options.headers["sec-fetch-mode"];
- }
- const res = await fetch(url, {
- ...options,
- headers: normalizeHeaders(options.headers)
- }).catch((error) => {
- throw new Error(`Failed to download ${url}: ${error}`, { cause: error });
- });
- if (options.validateStatus && res.status >= 400) {
- throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
- }
- return res;
- }
- function cacheDirectory() {
- return process.env.XDG_CACHE_HOME ? resolve(process.env.XDG_CACHE_HOME, "giget") : resolve(homedir(), ".cache/giget");
- }
- function normalizeHeaders(headers = {}) {
- const normalized = {};
- for (const [key, value] of Object.entries(headers)) {
- if (!value) {
- continue;
- }
- normalized[key.toLowerCase()] = value;
- }
- return normalized;
- }
- 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 http = async (input, options) => {
- if (input.endsWith(".json")) {
- return await _httpJSON(input, options);
- }
- const url = new URL(input);
- let name = basename(url.pathname);
- try {
- const head = await sendFetch(url.href, {
- method: "HEAD",
- validateStatus: true,
- headers: {
- authorization: options.auth ? `Bearer ${options.auth}` : void 0
- }
- });
- const _contentType = head.headers.get("content-type") || "";
- if (_contentType.includes("application/json")) {
- return await _httpJSON(input, options);
- }
- const filename = head.headers.get("content-disposition")?.match(/filename="?(.+)"?/)?.[1];
- if (filename) {
- name = filename.split(".")[0];
- }
- } catch (error) {
- debug(`Failed to fetch HEAD for ${url.href}:`, error);
- }
- return {
- name: `${name}-${url.href.slice(0, 8)}`,
- version: "",
- subdir: "",
- tar: url.href,
- defaultDir: name,
- headers: {
- Authorization: options.auth ? `Bearer ${options.auth}` : void 0
- }
- };
- };
- const _httpJSON = async (input, options) => {
- const result = await sendFetch(input, {
- validateStatus: true,
- headers: {
- authorization: options.auth ? `Bearer ${options.auth}` : void 0
- }
- });
- const info = await result.json();
- if (!info.tar || !info.name) {
- throw new Error(
- `Invalid template info from ${input}. name or tar fields are missing!`
- );
- }
- return info;
- };
- const github = (input, options) => {
- const parsed = parseGitURI(input);
- const githubAPIURL = process.env.GIGET_GITHUB_URL || "https://api.github.com";
- return {
- name: parsed.repo.replace("/", "-"),
- version: parsed.ref,
- subdir: parsed.subdir,
- headers: {
- Authorization: options.auth ? `Bearer ${options.auth}` : void 0,
- Accept: "application/vnd.github+json",
- "X-GitHub-Api-Version": "2022-11-28"
- },
- url: `${githubAPIURL.replace("api.github.com", "github.com")}/${parsed.repo}/tree/${parsed.ref}${parsed.subdir}`,
- tar: `${githubAPIURL}/repos/${parsed.repo}/tarball/${parsed.ref}`
- };
- };
- const gitlab = (input, options) => {
- const parsed = parseGitURI(input);
- const gitlab2 = process.env.GIGET_GITLAB_URL || "https://gitlab.com";
- return {
- name: parsed.repo.replace("/", "-"),
- version: parsed.ref,
- subdir: parsed.subdir,
- headers: {
- authorization: options.auth ? `Bearer ${options.auth}` : void 0,
- // https://gitlab.com/gitlab-org/gitlab/-/commit/50c11f278d18fe1f3fb12eb595067216bb58ade2
- "sec-fetch-mode": "same-origin"
- },
- url: `${gitlab2}/${parsed.repo}/tree/${parsed.ref}${parsed.subdir}`,
- tar: `${gitlab2}/${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 = {
- http,
- https: http,
- github,
- gh: github,
- gitlab,
- bitbucket,
- sourcehut
- };
- const DEFAULT_REGISTRY = "https://raw.githubusercontent.com/unjs/giget/main/templates";
- const registryProvider = (registryEndpoint = DEFAULT_REGISTRY, options = {}) => {
- return async (input) => {
- const start = Date.now();
- const registryURL = `${registryEndpoint}/${input}.json`;
- const result = await sendFetch(registryURL, {
- headers: {
- authorization: options.auth ? `Bearer ${options.auth}` : void 0
- }
- });
- 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 ? void 0 : registryProvider(options.registry, { auth: options.auth });
- let providerName = options.provider || (registry ? "registry" : "github");
- let source = input;
- const sourceProvierMatch = input.match(sourceProtoRe);
- if (sourceProvierMatch) {
- providerName = sourceProvierMatch[1];
- source = input.slice(sourceProvierMatch[0].length);
- if (providerName === "http" || providerName === "https") {
- source = input;
- }
- }
- 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}`
- );
- });
- if (!template) {
- throw new Error(`Failed to resolve template from ${providerName}`);
- }
- template.name = (template.name || "template").replace(/[^\da-z-]/gi, "-");
- template.defaultDir = (template.defaultDir || template.name).replace(
- /[^\da-z-]/gi,
- "-"
- );
- const temporaryDirectory = resolve(
- cacheDirectory(),
- providerName,
- 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: {
- Authorization: options.auth ? `Bearer ${options.auth}` : void 0,
- ...normalizeHeaders(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 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 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`);
- if (options.install) {
- debug("Installing dependencies...");
- await installDependencies({
- cwd: extractPath,
- silent: options.silent
- });
- }
- return {
- ...template,
- source,
- dir: extractPath
- };
- }
- export { downloadTemplate, registryProvider, startShell };
|