utils.js 9.1 KB

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