utils.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { join, dirname, readJson } = require("../util/fs");
  7. /** @typedef {import("../util/fs").InputFileSystem} InputFileSystem */
  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. * @param {string} pathname pathname
  42. * @param {string} hash hash
  43. * @returns {string | undefined} hash
  44. */
  45. "github.com": (pathname, hash) => {
  46. let [, user, project, type, commithash] = pathname.split("/", 5);
  47. if (type && type !== "tree") {
  48. return;
  49. }
  50. if (!type) {
  51. commithash = hash;
  52. } else {
  53. commithash = "#" + commithash;
  54. }
  55. if (project && project.endsWith(".git")) {
  56. project = project.slice(0, -4);
  57. }
  58. if (!user || !project) {
  59. return;
  60. }
  61. return commithash;
  62. },
  63. /**
  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. * @param {string} pathname pathname
  86. * @param {string} hash hash
  87. * @returns {string | undefined} hash
  88. */
  89. "bitbucket.org": (pathname, hash) => {
  90. let [, user, project, aux] = pathname.split("/", 4);
  91. if (["get"].includes(aux)) {
  92. return;
  93. }
  94. if (project && project.endsWith(".git")) {
  95. project = project.slice(0, -4);
  96. }
  97. if (!user || !project) {
  98. return;
  99. }
  100. return hash;
  101. },
  102. /**
  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. *
  127. * @inner
  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 (e) {}
  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. *
  154. * @inner
  155. * @param {string} gitUrl git url
  156. * @returns {string} fixed url
  157. */
  158. function correctUrl(gitUrl) {
  159. // like:
  160. // proto://hostname.com:user/repo -> proto://hostname.com/user/repo
  161. return gitUrl.replace(RE_HOSTNAME_WITH_COLON, "$1/$2");
  162. }
  163. /**
  164. * make url protocol right for URL parse
  165. *
  166. * @inner
  167. * @param {string} gitUrl git url
  168. * @returns {string} fixed url
  169. */
  170. function correctProtocol(gitUrl) {
  171. // eg: github:foo/bar#v1.0. Should not add double slash, in case of error parsed `pathname`
  172. if (RE_GIT_URL_SHORT.test(gitUrl)) {
  173. return gitUrl;
  174. }
  175. // eg: user@github.com:foo/bar
  176. if (!RE_CUSTOM_PROTOCOL.test(gitUrl)) {
  177. return `${DEF_GIT_PROTOCOL}${gitUrl}`;
  178. }
  179. return gitUrl;
  180. }
  181. /**
  182. * extract git dep version from hash
  183. *
  184. * @inner
  185. * @param {string} hash hash
  186. * @returns {string} git dep version
  187. */
  188. function getVersionFromHash(hash) {
  189. const matched = hash.match(RE_URL_HASH_VERSION);
  190. return (matched && matched[1]) || "";
  191. }
  192. /**
  193. * if string can be decoded
  194. *
  195. * @inner
  196. * @param {string} str str to be checked
  197. * @returns {boolean} if can be decoded
  198. */
  199. function canBeDecoded(str) {
  200. try {
  201. decodeURIComponent(str);
  202. } catch (e) {
  203. return false;
  204. }
  205. return true;
  206. }
  207. /**
  208. * get right dep version from git url
  209. *
  210. * @inner
  211. * @param {string} gitUrl git url
  212. * @returns {string} dep version
  213. */
  214. function getGitUrlVersion(gitUrl) {
  215. let oriGitUrl = gitUrl;
  216. // github extreme shorthand
  217. if (RE_URL_GITHUB_EXTREME_SHORT.test(gitUrl)) {
  218. gitUrl = "github:" + gitUrl;
  219. } else {
  220. gitUrl = correctProtocol(gitUrl);
  221. }
  222. gitUrl = correctUrl(gitUrl);
  223. let parsed;
  224. try {
  225. parsed = new URL(gitUrl);
  226. // eslint-disable-next-line no-empty
  227. } catch (e) {}
  228. if (!parsed) {
  229. return "";
  230. }
  231. const { protocol, hostname, pathname, username, password } = parsed;
  232. if (!RE_PROTOCOL.test(protocol)) {
  233. return "";
  234. }
  235. // pathname shouldn't be empty or URL malformed
  236. if (!pathname || !canBeDecoded(pathname)) {
  237. return "";
  238. }
  239. // without protocol, there should have auth info
  240. if (RE_NO_PROTOCOL.test(oriGitUrl) && !username && !password) {
  241. return "";
  242. }
  243. if (!PROTOCOLS_FOR_SHORT.includes(protocol.toLowerCase())) {
  244. if (!RE_HOSTNAME.test(hostname)) {
  245. return "";
  246. }
  247. const commithash = getCommithash(parsed);
  248. return getVersionFromHash(commithash) || commithash;
  249. }
  250. // for protocol short
  251. return getVersionFromHash(gitUrl);
  252. }
  253. /**
  254. * @param {string} str maybe required version
  255. * @returns {boolean} true, if it looks like a version
  256. */
  257. function isRequiredVersion(str) {
  258. return VERSION_PATTERN_REGEXP.test(str);
  259. }
  260. exports.isRequiredVersion = isRequiredVersion;
  261. /**
  262. * @see https://docs.npmjs.com/cli/v7/configuring-npm/package-json#urls-as-dependencies
  263. * @param {string} versionDesc version to be normalized
  264. * @returns {string} normalized version
  265. */
  266. function normalizeVersion(versionDesc) {
  267. versionDesc = (versionDesc && versionDesc.trim()) || "";
  268. if (isRequiredVersion(versionDesc)) {
  269. return versionDesc;
  270. }
  271. // add handle for URL Dependencies
  272. return getGitUrlVersion(versionDesc.toLowerCase());
  273. }
  274. exports.normalizeVersion = normalizeVersion;
  275. /**
  276. *
  277. * @param {InputFileSystem} fs file system
  278. * @param {string} directory directory to start looking into
  279. * @param {string[]} descriptionFiles possible description filenames
  280. * @param {function((Error | null)=, {data: object, path: string}=): void} callback callback
  281. */
  282. const getDescriptionFile = (fs, directory, descriptionFiles, callback) => {
  283. let i = 0;
  284. const tryLoadCurrent = () => {
  285. if (i >= descriptionFiles.length) {
  286. const parentDirectory = dirname(fs, directory);
  287. if (!parentDirectory || parentDirectory === directory) return callback();
  288. return getDescriptionFile(
  289. fs,
  290. parentDirectory,
  291. descriptionFiles,
  292. callback
  293. );
  294. }
  295. const filePath = join(fs, directory, descriptionFiles[i]);
  296. readJson(fs, filePath, (err, data) => {
  297. if (err) {
  298. if ("code" in err && err.code === "ENOENT") {
  299. i++;
  300. return tryLoadCurrent();
  301. }
  302. return callback(err);
  303. }
  304. if (!data || typeof data !== "object" || Array.isArray(data)) {
  305. return callback(
  306. new Error(`Description file ${filePath} is not an object`)
  307. );
  308. }
  309. callback(null, { data, path: filePath });
  310. });
  311. };
  312. tryLoadCurrent();
  313. };
  314. exports.getDescriptionFile = getDescriptionFile;
  315. exports.getRequiredVersionFromDescriptionFile = (data, packageName) => {
  316. if (
  317. data.optionalDependencies &&
  318. typeof data.optionalDependencies === "object" &&
  319. packageName in data.optionalDependencies
  320. ) {
  321. return normalizeVersion(data.optionalDependencies[packageName]);
  322. }
  323. if (
  324. data.dependencies &&
  325. typeof data.dependencies === "object" &&
  326. packageName in data.dependencies
  327. ) {
  328. return normalizeVersion(data.dependencies[packageName]);
  329. }
  330. if (
  331. data.peerDependencies &&
  332. typeof data.peerDependencies === "object" &&
  333. packageName in data.peerDependencies
  334. ) {
  335. return normalizeVersion(data.peerDependencies[packageName]);
  336. }
  337. if (
  338. data.devDependencies &&
  339. typeof data.devDependencies === "object" &&
  340. packageName in data.devDependencies
  341. ) {
  342. return normalizeVersion(data.devDependencies[packageName]);
  343. }
  344. };