utils.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { dirname, join, readJson } = require("../util/fs");
  7. /** @import { InputFileSystem, JsonObject, JsonPrimitive } from "../util/fs" */
  8. // Extreme shorthand only for github. eg: foo/bar
  9. const RE_URL_GITHUB_EXTREME_SHORT = /^[^/@:.\s][^/@:\s]*\/[^@:\s]*[^/@:\s]#\S+/;
  10. // Short url with specific protocol. eg: github:foo/bar
  11. const RE_GIT_URL_SHORT = /^(?:github|gitlab|bitbucket|gist):\/?[^/.]+\/?/i;
  12. // Currently supported protocols
  13. const RE_PROTOCOL =
  14. /^(?:(?:git\+)?(?:ssh|https?|file)|git|github|gitlab|bitbucket|gist):$/i;
  15. // Has custom protocol
  16. const RE_CUSTOM_PROTOCOL = /^(?:(?:git\+)?(?:ssh|https?|file)|git):\/\//i;
  17. // Valid hash format for npm / yarn ...
  18. const RE_URL_HASH_VERSION = /#(?:semver:)?(.+)/;
  19. // Simple hostname validate
  20. const RE_HOSTNAME = /^(?:[^/.]+(?:\.[^/.]+)+|localhost)$/;
  21. // For hostname with colon. eg: ssh://user@github.com:foo/bar
  22. const RE_HOSTNAME_WITH_COLON =
  23. /([^/@#:.]+(?:\.[^/@#:.]+)+|localhost):([^#/0-9]+)/;
  24. // Reg for url without protocol
  25. const RE_NO_PROTOCOL = /^[^/@#:.]+(?:\.[^/@#:.]+)+/;
  26. // RegExp for version string
  27. const VERSION_PATTERN_REGEXP = /^(?:[\d^=v<>~]|[*xX]$)/;
  28. // Specific protocol for short url without normal hostname
  29. const PROTOCOLS_FOR_SHORT = [
  30. "github:",
  31. "gitlab:",
  32. "bitbucket:",
  33. "gist:",
  34. "file:"
  35. ];
  36. // Default protocol for git url
  37. const DEF_GIT_PROTOCOL = "git+ssh://";
  38. // thanks to https://github.com/npm/hosted-git-info/blob/latest/git-host-info.js
  39. const extractCommithashByDomain = {
  40. /**
  41. * Returns hash.
  42. * @param {string} pathname pathname
  43. * @param {string} hash hash
  44. * @returns {string | undefined} hash
  45. */
  46. "github.com": (pathname, hash) => {
  47. let [, user, project, type, commithash] = pathname.split("/", 5);
  48. if (type && type !== "tree") {
  49. return;
  50. }
  51. commithash = !type ? hash : `#${commithash}`;
  52. if (project && project.endsWith(".git")) {
  53. project = project.slice(0, -4);
  54. }
  55. if (!user || !project) {
  56. return;
  57. }
  58. return commithash;
  59. },
  60. /**
  61. * Returns hash.
  62. * @param {string} pathname pathname
  63. * @param {string} hash hash
  64. * @returns {string | undefined} hash
  65. */
  66. "gitlab.com": (pathname, hash) => {
  67. const path = pathname.slice(1);
  68. if (path.includes("/-/") || path.includes("/archive.tar.gz")) {
  69. return;
  70. }
  71. const segments = path.split("/");
  72. let project = /** @type {string} */ (segments.pop());
  73. if (project.endsWith(".git")) {
  74. project = project.slice(0, -4);
  75. }
  76. const user = segments.join("/");
  77. if (!user || !project) {
  78. return;
  79. }
  80. return hash;
  81. },
  82. /**
  83. * Returns hash.
  84. * @param {string} pathname pathname
  85. * @param {string} hash hash
  86. * @returns {string | undefined} hash
  87. */
  88. "bitbucket.org": (pathname, hash) => {
  89. let [, user, project, aux] = pathname.split("/", 4);
  90. if (["get"].includes(aux)) {
  91. return;
  92. }
  93. if (project && project.endsWith(".git")) {
  94. project = project.slice(0, -4);
  95. }
  96. if (!user || !project) {
  97. return;
  98. }
  99. return hash;
  100. },
  101. /**
  102. * Returns hash.
  103. * @param {string} pathname pathname
  104. * @param {string} hash hash
  105. * @returns {string | undefined} hash
  106. */
  107. "gist.github.com": (pathname, hash) => {
  108. let [, user, project, aux] = pathname.split("/", 4);
  109. if (aux === "raw") {
  110. return;
  111. }
  112. if (!project) {
  113. if (!user) {
  114. return;
  115. }
  116. project = user;
  117. }
  118. if (project.endsWith(".git")) {
  119. project = project.slice(0, -4);
  120. }
  121. return hash;
  122. }
  123. };
  124. /**
  125. * extract commit hash from parsed url
  126. * @param {URL} urlParsed parsed url
  127. * @returns {string} commithash
  128. */
  129. function getCommithash(urlParsed) {
  130. let { hostname, pathname, hash } = urlParsed;
  131. hostname = hostname.replace(/^www\./, "");
  132. try {
  133. hash = decodeURIComponent(hash);
  134. // eslint-disable-next-line no-empty
  135. } catch (_err) {}
  136. if (
  137. extractCommithashByDomain[
  138. /** @type {keyof extractCommithashByDomain} */ (hostname)
  139. ]
  140. ) {
  141. return (
  142. extractCommithashByDomain[
  143. /** @type {keyof extractCommithashByDomain} */ (hostname)
  144. ](pathname, hash) || ""
  145. );
  146. }
  147. return hash;
  148. }
  149. /**
  150. * make url right for URL parse
  151. * @param {string} gitUrl git url
  152. * @returns {string} fixed url
  153. */
  154. function correctUrl(gitUrl) {
  155. // like:
  156. // proto://hostname.com:user/repo -> proto://hostname.com/user/repo
  157. return gitUrl.replace(RE_HOSTNAME_WITH_COLON, "$1/$2");
  158. }
  159. /**
  160. * make url protocol right for URL parse
  161. * @param {string} gitUrl git url
  162. * @returns {string} fixed url
  163. */
  164. function correctProtocol(gitUrl) {
  165. // eg: github:foo/bar#v1.0. Should not add double slash, in case of error parsed `pathname`
  166. if (RE_GIT_URL_SHORT.test(gitUrl)) {
  167. return gitUrl;
  168. }
  169. // eg: user@github.com:foo/bar
  170. if (!RE_CUSTOM_PROTOCOL.test(gitUrl)) {
  171. return `${DEF_GIT_PROTOCOL}${gitUrl}`;
  172. }
  173. return gitUrl;
  174. }
  175. /**
  176. * extract git dep version from hash
  177. * @param {string} hash hash
  178. * @returns {string} git dep version
  179. */
  180. function getVersionFromHash(hash) {
  181. const matched = hash.match(RE_URL_HASH_VERSION);
  182. return (matched && matched[1]) || "";
  183. }
  184. /**
  185. * if string can be decoded
  186. * @param {string} str str to be checked
  187. * @returns {boolean} if can be decoded
  188. */
  189. function canBeDecoded(str) {
  190. try {
  191. decodeURIComponent(str);
  192. } catch (_err) {
  193. return false;
  194. }
  195. return true;
  196. }
  197. /**
  198. * get right dep version from git url
  199. * @param {string} gitUrl git url
  200. * @returns {string} dep version
  201. */
  202. function getGitUrlVersion(gitUrl) {
  203. const oriGitUrl = gitUrl;
  204. // github extreme shorthand
  205. gitUrl = RE_URL_GITHUB_EXTREME_SHORT.test(gitUrl)
  206. ? `github:${gitUrl}`
  207. : correctProtocol(gitUrl);
  208. gitUrl = correctUrl(gitUrl);
  209. /** @type {undefined | URL} */
  210. let parsed;
  211. try {
  212. parsed = new URL(gitUrl);
  213. // eslint-disable-next-line no-empty
  214. } catch (_err) {}
  215. if (!parsed) {
  216. return "";
  217. }
  218. const { protocol, hostname, pathname, username, password } = parsed;
  219. if (!RE_PROTOCOL.test(protocol)) {
  220. return "";
  221. }
  222. // pathname shouldn't be empty or URL malformed
  223. if (!pathname || !canBeDecoded(pathname)) {
  224. return "";
  225. }
  226. // without protocol, there should have auth info
  227. if (RE_NO_PROTOCOL.test(oriGitUrl) && !username && !password) {
  228. return "";
  229. }
  230. if (!PROTOCOLS_FOR_SHORT.includes(protocol.toLowerCase())) {
  231. if (!RE_HOSTNAME.test(hostname)) {
  232. return "";
  233. }
  234. const commithash = getCommithash(parsed);
  235. return getVersionFromHash(commithash) || commithash;
  236. }
  237. // for protocol short
  238. return getVersionFromHash(gitUrl);
  239. }
  240. /** @typedef {{ data: JsonObject, path: string }} DescriptionFile */
  241. /**
  242. * Gets description file.
  243. * @param {InputFileSystem} fs file system
  244. * @param {string} directory directory to start looking into
  245. * @param {string[]} descriptionFiles possible description filenames
  246. * @param {(err?: Error | null, descriptionFile?: DescriptionFile, paths?: string[]) => void} callback callback
  247. * @param {(descriptionFile?: DescriptionFile) => boolean} satisfiesDescriptionFileData file data compliance check
  248. * @param {Set<string>} checkedFilePaths set of file paths that have been checked
  249. */
  250. const getDescriptionFile = (
  251. fs,
  252. directory,
  253. descriptionFiles,
  254. callback,
  255. satisfiesDescriptionFileData,
  256. checkedFilePaths = new Set()
  257. ) => {
  258. let i = 0;
  259. const satisfiesDescriptionFileDataInternal = {
  260. check: satisfiesDescriptionFileData,
  261. checkedFilePaths
  262. };
  263. const tryLoadCurrent = () => {
  264. if (i >= descriptionFiles.length) {
  265. const parentDirectory = dirname(fs, directory);
  266. if (!parentDirectory || parentDirectory === directory) {
  267. return callback(null, undefined, [
  268. ...satisfiesDescriptionFileDataInternal.checkedFilePaths
  269. ]);
  270. }
  271. return getDescriptionFile(
  272. fs,
  273. parentDirectory,
  274. descriptionFiles,
  275. callback,
  276. satisfiesDescriptionFileDataInternal.check,
  277. satisfiesDescriptionFileDataInternal.checkedFilePaths
  278. );
  279. }
  280. const filePath = join(fs, directory, descriptionFiles[i]);
  281. readJson(fs, filePath, (err, data) => {
  282. if (err) {
  283. if ("code" in err && err.code === "ENOENT") {
  284. i++;
  285. return tryLoadCurrent();
  286. }
  287. return callback(err);
  288. }
  289. if (!data || typeof data !== "object" || Array.isArray(data)) {
  290. return callback(
  291. new Error(`Description file ${filePath} is not an object`)
  292. );
  293. }
  294. if (
  295. typeof satisfiesDescriptionFileDataInternal.check === "function" &&
  296. !satisfiesDescriptionFileDataInternal.check({ data, path: filePath })
  297. ) {
  298. i++;
  299. satisfiesDescriptionFileDataInternal.checkedFilePaths.add(filePath);
  300. return tryLoadCurrent();
  301. }
  302. callback(null, { data, path: filePath });
  303. });
  304. };
  305. tryLoadCurrent();
  306. };
  307. module.exports.getDescriptionFile = getDescriptionFile;
  308. /**
  309. * Gets required version from description file.
  310. * @param {JsonObject} data description file data i.e.: package.json
  311. * @param {string} packageName name of the dependency
  312. * @returns {string | undefined} normalized version
  313. */
  314. const getRequiredVersionFromDescriptionFile = (data, packageName) => {
  315. const dependencyTypes = [
  316. "optionalDependencies",
  317. "dependencies",
  318. "peerDependencies",
  319. "devDependencies"
  320. ];
  321. for (const dependencyType of dependencyTypes) {
  322. const dependency = /** @type {JsonObject} */ (data[dependencyType]);
  323. if (
  324. dependency &&
  325. typeof dependency === "object" &&
  326. packageName in dependency
  327. ) {
  328. return normalizeVersion(
  329. /** @type {Exclude<JsonPrimitive, null | boolean | number>} */ (
  330. dependency[packageName]
  331. )
  332. );
  333. }
  334. }
  335. };
  336. module.exports.getRequiredVersionFromDescriptionFile =
  337. getRequiredVersionFromDescriptionFile;
  338. /**
  339. * Checks whether this object is required version.
  340. * @param {string} str maybe required version
  341. * @returns {boolean} true, if it looks like a version
  342. */
  343. function isRequiredVersion(str) {
  344. return VERSION_PATTERN_REGEXP.test(str);
  345. }
  346. module.exports.isRequiredVersion = isRequiredVersion;
  347. /**
  348. * Normalizes version.
  349. * @see https://docs.npmjs.com/cli/v7/configuring-npm/package-json#urls-as-dependencies
  350. * @param {string} versionDesc version to be normalized
  351. * @returns {string} normalized version
  352. */
  353. function normalizeVersion(versionDesc) {
  354. versionDesc = (versionDesc && versionDesc.trim()) || "";
  355. if (isRequiredVersion(versionDesc)) {
  356. return versionDesc;
  357. }
  358. // add handle for URL Dependencies
  359. return getGitUrlVersion(versionDesc.toLowerCase());
  360. }
  361. module.exports.normalizeVersion = normalizeVersion;