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