utils.js 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  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. commithash = !type ? hash : `#${commithash}`;
  53. if (project && project.endsWith(".git")) {
  54. project = project.slice(0, -4);
  55. }
  56. if (!user || !project) {
  57. return;
  58. }
  59. return commithash;
  60. },
  61. /**
  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. * @param {string} pathname pathname
  84. * @param {string} hash hash
  85. * @returns {string | undefined} hash
  86. */
  87. "bitbucket.org": (pathname, hash) => {
  88. let [, user, project, aux] = pathname.split("/", 4);
  89. if (["get"].includes(aux)) {
  90. return;
  91. }
  92. if (project && project.endsWith(".git")) {
  93. project = project.slice(0, -4);
  94. }
  95. if (!user || !project) {
  96. return;
  97. }
  98. return hash;
  99. },
  100. /**
  101. * @param {string} pathname pathname
  102. * @param {string} hash hash
  103. * @returns {string | undefined} hash
  104. */
  105. "gist.github.com": (pathname, hash) => {
  106. let [, user, project, aux] = pathname.split("/", 4);
  107. if (aux === "raw") {
  108. return;
  109. }
  110. if (!project) {
  111. if (!user) {
  112. return;
  113. }
  114. project = user;
  115. }
  116. if (project.endsWith(".git")) {
  117. project = project.slice(0, -4);
  118. }
  119. return hash;
  120. }
  121. };
  122. /**
  123. * extract commit hash from parsed url
  124. * @inner
  125. * @param {URL} urlParsed parsed url
  126. * @returns {string} commithash
  127. */
  128. function getCommithash(urlParsed) {
  129. let { hostname, pathname, hash } = urlParsed;
  130. hostname = hostname.replace(/^www\./, "");
  131. try {
  132. hash = decodeURIComponent(hash);
  133. // eslint-disable-next-line no-empty
  134. } catch (_err) {}
  135. if (
  136. extractCommithashByDomain[
  137. /** @type {keyof extractCommithashByDomain} */ (hostname)
  138. ]
  139. ) {
  140. return (
  141. extractCommithashByDomain[
  142. /** @type {keyof extractCommithashByDomain} */ (hostname)
  143. ](pathname, hash) || ""
  144. );
  145. }
  146. return hash;
  147. }
  148. /**
  149. * make url right for URL parse
  150. * @inner
  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. * @inner
  162. * @param {string} gitUrl git url
  163. * @returns {string} fixed url
  164. */
  165. function correctProtocol(gitUrl) {
  166. // eg: github:foo/bar#v1.0. Should not add double slash, in case of error parsed `pathname`
  167. if (RE_GIT_URL_SHORT.test(gitUrl)) {
  168. return gitUrl;
  169. }
  170. // eg: user@github.com:foo/bar
  171. if (!RE_CUSTOM_PROTOCOL.test(gitUrl)) {
  172. return `${DEF_GIT_PROTOCOL}${gitUrl}`;
  173. }
  174. return gitUrl;
  175. }
  176. /**
  177. * extract git dep version from hash
  178. * @inner
  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. * @inner
  189. * @param {string} str str to be checked
  190. * @returns {boolean} if can be decoded
  191. */
  192. function canBeDecoded(str) {
  193. try {
  194. decodeURIComponent(str);
  195. } catch (_err) {
  196. return false;
  197. }
  198. return true;
  199. }
  200. /**
  201. * get right dep version from git url
  202. * @inner
  203. * @param {string} gitUrl git url
  204. * @returns {string} dep version
  205. */
  206. function getGitUrlVersion(gitUrl) {
  207. const oriGitUrl = gitUrl;
  208. // github extreme shorthand
  209. gitUrl = RE_URL_GITHUB_EXTREME_SHORT.test(gitUrl)
  210. ? `github:${gitUrl}`
  211. : correctProtocol(gitUrl);
  212. gitUrl = correctUrl(gitUrl);
  213. let parsed;
  214. try {
  215. parsed = new URL(gitUrl);
  216. // eslint-disable-next-line no-empty
  217. } catch (_err) {}
  218. if (!parsed) {
  219. return "";
  220. }
  221. const { protocol, hostname, pathname, username, password } = parsed;
  222. if (!RE_PROTOCOL.test(protocol)) {
  223. return "";
  224. }
  225. // pathname shouldn't be empty or URL malformed
  226. if (!pathname || !canBeDecoded(pathname)) {
  227. return "";
  228. }
  229. // without protocol, there should have auth info
  230. if (RE_NO_PROTOCOL.test(oriGitUrl) && !username && !password) {
  231. return "";
  232. }
  233. if (!PROTOCOLS_FOR_SHORT.includes(protocol.toLowerCase())) {
  234. if (!RE_HOSTNAME.test(hostname)) {
  235. return "";
  236. }
  237. const commithash = getCommithash(parsed);
  238. return getVersionFromHash(commithash) || commithash;
  239. }
  240. // for protocol short
  241. return getVersionFromHash(gitUrl);
  242. }
  243. /**
  244. * @param {string} str maybe required version
  245. * @returns {boolean} true, if it looks like a version
  246. */
  247. function isRequiredVersion(str) {
  248. return VERSION_PATTERN_REGEXP.test(str);
  249. }
  250. module.exports.isRequiredVersion = isRequiredVersion;
  251. /**
  252. * @see https://docs.npmjs.com/cli/v7/configuring-npm/package-json#urls-as-dependencies
  253. * @param {string} versionDesc version to be normalized
  254. * @returns {string} normalized version
  255. */
  256. function normalizeVersion(versionDesc) {
  257. versionDesc = (versionDesc && versionDesc.trim()) || "";
  258. if (isRequiredVersion(versionDesc)) {
  259. return versionDesc;
  260. }
  261. // add handle for URL Dependencies
  262. return getGitUrlVersion(versionDesc.toLowerCase());
  263. }
  264. module.exports.normalizeVersion = normalizeVersion;
  265. /** @typedef {{ data: JsonObject, path: string }} DescriptionFile */
  266. /**
  267. * @param {InputFileSystem} fs file system
  268. * @param {string} directory directory to start looking into
  269. * @param {string[]} descriptionFiles possible description filenames
  270. * @param {function((Error | null)=, DescriptionFile=, string[]=): void} callback callback
  271. * @param {function(DescriptionFile=): boolean} satisfiesDescriptionFileData file data compliance check
  272. * @param {Set<string>} checkedFilePaths set of file paths that have been checked
  273. */
  274. const getDescriptionFile = (
  275. fs,
  276. directory,
  277. descriptionFiles,
  278. callback,
  279. satisfiesDescriptionFileData,
  280. checkedFilePaths = new Set()
  281. ) => {
  282. let i = 0;
  283. const satisfiesDescriptionFileDataInternal = {
  284. check: satisfiesDescriptionFileData,
  285. checkedFilePaths
  286. };
  287. const tryLoadCurrent = () => {
  288. if (i >= descriptionFiles.length) {
  289. const parentDirectory = dirname(fs, directory);
  290. if (!parentDirectory || parentDirectory === directory) {
  291. return callback(
  292. null,
  293. undefined,
  294. Array.from(satisfiesDescriptionFileDataInternal.checkedFilePaths)
  295. );
  296. }
  297. return getDescriptionFile(
  298. fs,
  299. parentDirectory,
  300. descriptionFiles,
  301. callback,
  302. satisfiesDescriptionFileDataInternal.check,
  303. satisfiesDescriptionFileDataInternal.checkedFilePaths
  304. );
  305. }
  306. const filePath = join(fs, directory, descriptionFiles[i]);
  307. readJson(fs, filePath, (err, data) => {
  308. if (err) {
  309. if ("code" in err && err.code === "ENOENT") {
  310. i++;
  311. return tryLoadCurrent();
  312. }
  313. return callback(err);
  314. }
  315. if (!data || typeof data !== "object" || Array.isArray(data)) {
  316. return callback(
  317. new Error(`Description file ${filePath} is not an object`)
  318. );
  319. }
  320. if (
  321. typeof satisfiesDescriptionFileDataInternal.check === "function" &&
  322. !satisfiesDescriptionFileDataInternal.check({ data, path: filePath })
  323. ) {
  324. i++;
  325. satisfiesDescriptionFileDataInternal.checkedFilePaths.add(filePath);
  326. return tryLoadCurrent();
  327. }
  328. callback(null, { data, path: filePath });
  329. });
  330. };
  331. tryLoadCurrent();
  332. };
  333. module.exports.getDescriptionFile = getDescriptionFile;
  334. /**
  335. * @param {JsonObject} data description file data i.e.: package.json
  336. * @param {string} packageName name of the dependency
  337. * @returns {string | undefined} normalized version
  338. */
  339. const getRequiredVersionFromDescriptionFile = (data, packageName) => {
  340. const dependencyTypes = [
  341. "optionalDependencies",
  342. "dependencies",
  343. "peerDependencies",
  344. "devDependencies"
  345. ];
  346. for (const dependencyType of dependencyTypes) {
  347. const dependency = /** @type {JsonObject} */ (data[dependencyType]);
  348. if (
  349. dependency &&
  350. typeof dependency === "object" &&
  351. packageName in dependency
  352. ) {
  353. return normalizeVersion(
  354. /** @type {Exclude<JsonPrimitive, null | boolean| number>} */ (
  355. dependency[packageName]
  356. )
  357. );
  358. }
  359. }
  360. };
  361. module.exports.getRequiredVersionFromDescriptionFile =
  362. getRequiredVersionFromDescriptionFile;