版博士V2.0程序
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

292 řádky
9.1 KiB

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