utils.js 10 KB

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