LinkResolver.js 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const fs = require("fs");
  7. const path = require("path");
  8. // macOS, Linux, and Windows all rely on these errors
  9. const EXPECTED_ERRORS = new Set(["EINVAL", "ENOENT"]);
  10. // On Windows there is also this error in some cases
  11. if (process.platform === "win32") EXPECTED_ERRORS.add("UNKNOWN");
  12. class LinkResolver {
  13. constructor() {
  14. this.cache = new Map();
  15. }
  16. /**
  17. * @param {string} file path to file or directory
  18. * @returns {string[]} array of file and all symlinks contributed in the resolving process (first item is the resolved file)
  19. */
  20. resolve(file) {
  21. const cacheEntry = this.cache.get(file);
  22. if (cacheEntry !== undefined) {
  23. return cacheEntry;
  24. }
  25. const parent = path.dirname(file);
  26. if (parent === file) {
  27. // At root of filesystem there can't be a link
  28. const result = Object.freeze([file]);
  29. this.cache.set(file, result);
  30. return result;
  31. }
  32. // resolve the parent directory to find links there and get the real path
  33. const parentResolved = this.resolve(parent);
  34. let realFile = file;
  35. // is the parent directory really somewhere else?
  36. if (parentResolved[0] !== parent) {
  37. // get the real location of file
  38. const basename = path.basename(file);
  39. realFile = path.resolve(parentResolved[0], basename);
  40. }
  41. // try to read the link content
  42. try {
  43. const linkContent = fs.readlinkSync(realFile);
  44. // resolve the link content relative to the parent directory
  45. const resolvedLink = path.resolve(parentResolved[0], linkContent);
  46. // recursive resolve the link content for more links in the structure
  47. const linkResolved = this.resolve(resolvedLink);
  48. // merge parent and link resolve results
  49. let result;
  50. if (linkResolved.length > 1 && parentResolved.length > 1) {
  51. // when both contain links we need to duplicate them with a Set
  52. const resultSet = new Set(linkResolved);
  53. // add the link
  54. resultSet.add(realFile);
  55. // add all symlinks of the parent
  56. for (let i = 1; i < parentResolved.length; i++) {
  57. resultSet.add(parentResolved[i]);
  58. }
  59. result = Object.freeze(Array.from(resultSet));
  60. } else if (parentResolved.length > 1) {
  61. // we have links in the parent but not for the link content location
  62. result = parentResolved.slice();
  63. result[0] = linkResolved[0];
  64. // add the link
  65. result.push(realFile);
  66. Object.freeze(result);
  67. } else if (linkResolved.length > 1) {
  68. // we can return the link content location result
  69. result = linkResolved.slice();
  70. // add the link
  71. result.push(realFile);
  72. Object.freeze(result);
  73. } else {
  74. // neither link content location nor parent have links
  75. // this link is the only link here
  76. result = Object.freeze([
  77. // the resolve real location
  78. linkResolved[0],
  79. // add the link
  80. realFile
  81. ]);
  82. }
  83. this.cache.set(file, result);
  84. return result;
  85. } catch (e) {
  86. if (!EXPECTED_ERRORS.has(e.code)) {
  87. throw e;
  88. }
  89. // no link
  90. const result = parentResolved.slice();
  91. result[0] = realFile;
  92. Object.freeze(result);
  93. this.cache.set(file, result);
  94. return result;
  95. }
  96. }
  97. }
  98. module.exports = LinkResolver;