版博士V2.0程序
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 

288 rindas
8.9 KiB

  1. import { readFile, writeFile, rm, mkdir } from 'node:fs/promises';
  2. import { existsSync, createWriteStream, readdirSync } from 'node:fs';
  3. import { extract } from 'tar';
  4. import { resolve, relative, dirname } from 'pathe';
  5. import { defu } from 'defu';
  6. import { pipeline } from 'node:stream';
  7. import { spawnSync } from 'node:child_process';
  8. import { homedir } from 'node:os';
  9. import { promisify } from 'node:util';
  10. import { fetch } from 'node-fetch-native';
  11. import createHttpsProxyAgent from 'https-proxy-agent';
  12. async function download(url, filePath, options = {}) {
  13. const infoPath = filePath + ".json";
  14. const info = JSON.parse(
  15. await readFile(infoPath, "utf8").catch(() => "{}")
  16. );
  17. const headResponse = await sendFetch(url, {
  18. method: "HEAD",
  19. headers: options.headers
  20. }).catch(() => void 0);
  21. const etag = headResponse?.headers.get("etag");
  22. if (info.etag === etag && existsSync(filePath)) {
  23. return;
  24. }
  25. info.etag = etag;
  26. const response = await sendFetch(url, { headers: options.headers });
  27. if (response.status >= 400) {
  28. throw new Error(
  29. `Failed to download ${url}: ${response.status} ${response.statusText}`
  30. );
  31. }
  32. const stream = createWriteStream(filePath);
  33. await promisify(pipeline)(response.body, stream);
  34. await writeFile(infoPath, JSON.stringify(info), "utf8");
  35. }
  36. const inputRegex = /^(?<repo>[\w.-]+\/[\w.-]+)(?<subdir>[^#]+)?(?<ref>#[\w.-]+)?/;
  37. function parseGitURI(input) {
  38. const m = input.match(inputRegex)?.groups;
  39. return {
  40. repo: m.repo,
  41. subdir: m.subdir || "/",
  42. ref: m.ref ? m.ref.slice(1) : "main"
  43. };
  44. }
  45. function debug(...arguments_) {
  46. if (process.env.DEBUG) {
  47. console.debug("[giget]", ...arguments_);
  48. }
  49. }
  50. async function sendFetch(url, options = {}) {
  51. if (!options.agent) {
  52. const proxyEnv = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
  53. if (proxyEnv) {
  54. options.agent = createHttpsProxyAgent(proxyEnv);
  55. }
  56. }
  57. if (options?.headers) {
  58. options.headers = normalizeHeaders(options.headers);
  59. }
  60. return await fetch(url, options);
  61. }
  62. function cacheDirectory() {
  63. return process.env.XDG_CACHE_HOME ? resolve(process.env.XDG_CACHE_HOME, "giget") : resolve(homedir(), ".cache/giget");
  64. }
  65. function normalizeHeaders(headers = {}) {
  66. const normalized = {};
  67. for (const [key, value] of Object.entries(headers)) {
  68. if (!value) {
  69. continue;
  70. }
  71. normalized[key.toLowerCase()] = value;
  72. }
  73. return normalized;
  74. }
  75. function currentShell() {
  76. if (process.env.SHELL) {
  77. return process.env.SHELL;
  78. }
  79. if (process.platform === "win32") {
  80. return "cmd.exe";
  81. }
  82. return "/bin/bash";
  83. }
  84. function startShell(cwd) {
  85. cwd = resolve(cwd);
  86. const shell = currentShell();
  87. console.info(
  88. `(experimental) Opening shell in ${relative(process.cwd(), cwd)}...`
  89. );
  90. spawnSync(shell, [], {
  91. cwd,
  92. shell: true,
  93. stdio: "inherit"
  94. });
  95. }
  96. const github = (input, options) => {
  97. const parsed = parseGitURI(input);
  98. const github2 = process.env.GIGET_GITHUB_URL || "https://github.com";
  99. return {
  100. name: parsed.repo.replace("/", "-"),
  101. version: parsed.ref,
  102. subdir: parsed.subdir,
  103. headers: {
  104. authorization: options.auth ? `Bearer ${options.auth}` : void 0
  105. },
  106. url: `${github2}/${parsed.repo}/tree/${parsed.ref}${parsed.subdir}`,
  107. tar: `${github2}/${parsed.repo}/archive/${parsed.ref}.tar.gz`
  108. };
  109. };
  110. const gitlab = (input, options) => {
  111. const parsed = parseGitURI(input);
  112. const gitlab2 = process.env.GIGET_GITLAB_URL || "https://gitlab.com";
  113. return {
  114. name: parsed.repo.replace("/", "-"),
  115. version: parsed.ref,
  116. subdir: parsed.subdir,
  117. headers: {
  118. authorization: options.auth ? `Bearer ${options.auth}` : void 0
  119. },
  120. url: `${gitlab2}/${parsed.repo}/tree/${parsed.ref}${parsed.subdir}`,
  121. tar: `${gitlab2}/${parsed.repo}/-/archive/${parsed.ref}.tar.gz`
  122. };
  123. };
  124. const bitbucket = (input, options) => {
  125. const parsed = parseGitURI(input);
  126. return {
  127. name: parsed.repo.replace("/", "-"),
  128. version: parsed.ref,
  129. subdir: parsed.subdir,
  130. headers: {
  131. authorization: options.auth ? `Bearer ${options.auth}` : void 0
  132. },
  133. url: `https://bitbucket.com/${parsed.repo}/src/${parsed.ref}${parsed.subdir}`,
  134. tar: `https://bitbucket.org/${parsed.repo}/get/${parsed.ref}.tar.gz`
  135. };
  136. };
  137. const sourcehut = (input, options) => {
  138. const parsed = parseGitURI(input);
  139. return {
  140. name: parsed.repo.replace("/", "-"),
  141. version: parsed.ref,
  142. subdir: parsed.subdir,
  143. headers: {
  144. authorization: options.auth ? `Bearer ${options.auth}` : void 0
  145. },
  146. url: `https://git.sr.ht/~${parsed.repo}/tree/${parsed.ref}/item${parsed.subdir}`,
  147. tar: `https://git.sr.ht/~${parsed.repo}/archive/${parsed.ref}.tar.gz`
  148. };
  149. };
  150. const providers = {
  151. github,
  152. gh: github,
  153. gitlab,
  154. bitbucket,
  155. sourcehut
  156. };
  157. const DEFAULT_REGISTRY = "https://raw.githubusercontent.com/unjs/giget/main/templates";
  158. const registryProvider = (registryEndpoint = DEFAULT_REGISTRY, options) => {
  159. options = options || {};
  160. return async (input) => {
  161. const start = Date.now();
  162. const registryURL = `${registryEndpoint}/${input}.json`;
  163. const result = await sendFetch(registryURL, {
  164. headers: {
  165. authorization: options.auth ? `Bearer ${options.auth}` : void 0
  166. }
  167. });
  168. if (result.status >= 400) {
  169. throw new Error(
  170. `Failed to download ${input} template info from ${registryURL}: ${result.status} ${result.statusText}`
  171. );
  172. }
  173. const info = await result.json();
  174. if (!info.tar || !info.name) {
  175. throw new Error(
  176. `Invalid template info from ${registryURL}. name or tar fields are missing!`
  177. );
  178. }
  179. debug(
  180. `Fetched ${input} template info from ${registryURL} in ${Date.now() - start}ms`
  181. );
  182. return info;
  183. };
  184. };
  185. const sourceProtoRe = /^([\w-.]+):/;
  186. async function downloadTemplate(input, options = {}) {
  187. options = defu(
  188. {
  189. registry: process.env.GIGET_REGISTRY,
  190. auth: process.env.GIGET_AUTH
  191. },
  192. options
  193. );
  194. const registry = options.registry !== false ? registryProvider(options.registry, { auth: options.auth }) : void 0;
  195. let providerName = options.provider || (registryProvider ? "registry" : "github");
  196. let source = input;
  197. const sourceProvierMatch = input.match(sourceProtoRe);
  198. if (sourceProvierMatch) {
  199. providerName = sourceProvierMatch[1];
  200. source = input.slice(sourceProvierMatch[0].length);
  201. }
  202. const provider = options.providers?.[providerName] || providers[providerName] || registry;
  203. if (!provider) {
  204. throw new Error(`Unsupported provider: ${providerName}`);
  205. }
  206. const template = await Promise.resolve().then(() => provider(source, { auth: options.auth })).catch((error) => {
  207. throw new Error(
  208. `Failed to download template from ${providerName}: ${error.message}`
  209. );
  210. });
  211. template.name = (template.name || "template").replace(/[^\da-z-]/gi, "-");
  212. template.defaultDir = (template.defaultDir || template.name).replace(
  213. /[^\da-z-]/gi,
  214. "-"
  215. );
  216. const cwd = resolve(options.cwd || ".");
  217. const extractPath = resolve(cwd, options.dir || template.defaultDir);
  218. if (options.forceClean) {
  219. await rm(extractPath, { recursive: true, force: true });
  220. }
  221. if (!options.force && existsSync(extractPath) && readdirSync(extractPath).length > 0) {
  222. throw new Error(`Destination ${extractPath} already exists.`);
  223. }
  224. await mkdir(extractPath, { recursive: true });
  225. const temporaryDirectory = resolve(
  226. cacheDirectory(),
  227. options.provider,
  228. template.name
  229. );
  230. const tarPath = resolve(
  231. temporaryDirectory,
  232. (template.version || template.name) + ".tar.gz"
  233. );
  234. if (options.preferOffline && existsSync(tarPath)) {
  235. options.offline = true;
  236. }
  237. if (!options.offline) {
  238. await mkdir(dirname(tarPath), { recursive: true });
  239. const s2 = Date.now();
  240. await download(template.tar, tarPath, {
  241. headers: {
  242. authorization: options.auth ? `Bearer ${options.auth}` : void 0,
  243. ...normalizeHeaders(template.headers)
  244. }
  245. }).catch((error) => {
  246. if (!existsSync(tarPath)) {
  247. throw error;
  248. }
  249. debug("Download error. Using cached version:", error);
  250. options.offline = true;
  251. });
  252. debug(`Downloaded ${template.tar} to ${tarPath} in ${Date.now() - s2}ms`);
  253. }
  254. if (!existsSync(tarPath)) {
  255. throw new Error(
  256. `Tarball not found: ${tarPath} (offline: ${options.offline})`
  257. );
  258. }
  259. const s = Date.now();
  260. const subdir = template.subdir?.replace(/^\//, "") || "";
  261. await extract({
  262. file: tarPath,
  263. cwd: extractPath,
  264. onentry(entry) {
  265. entry.path = entry.path.split("/").splice(1).join("/");
  266. if (subdir) {
  267. if (entry.path.startsWith(subdir + "/")) {
  268. entry.path = entry.path.slice(subdir.length);
  269. } else {
  270. entry.path = "";
  271. }
  272. }
  273. }
  274. });
  275. debug(`Extracted to ${extractPath} in ${Date.now() - s}ms`);
  276. return {
  277. ...template,
  278. source,
  279. dir: extractPath
  280. };
  281. }
  282. export { downloadTemplate as d, registryProvider as r, startShell as s };