enhanced-relative.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. 'use strict';
  2. var fs = require('fs'),
  3. path = require('path');
  4. var cache;
  5. /**
  6. * Perform <code>path.relative()</code> but try to detect and correct sym-linked node modules.
  7. * @param {string} from The base path
  8. * @param {string} to The full path
  9. */
  10. function enhancedRelative(from, to) {
  11. // relative path
  12. var relative = path.relative(from, to);
  13. // trailing is the relative path portion without any '../'
  14. var trailing = relative.replace(/^\.{2}[\\\/]/, ''),
  15. leading = to.replace(trailing, '');
  16. // within project is what we want
  17. var isInProject = (relative === trailing);
  18. if (isInProject) {
  19. return relative;
  20. }
  21. // otherwise look at symbolic linked modules
  22. else {
  23. var splitTrailing = trailing.split(/[\\\/]/);
  24. // ensure failures can retry with fresh cache
  25. for (var i = cache ? 2 : 1, foundPath = false; (i > 0) && !foundPath; i--) {
  26. // ensure cache
  27. cache = cache || indexLinkedModules(from);
  28. // take elements from the trailing path and append them the the leading path in an attempt to find a package.json
  29. for (var j = 0; (j < splitTrailing.length) && !foundPath; j++) {
  30. // find the name of packages in the actual file location
  31. // start at the lowest concrete directory that appears in the relative path
  32. var packagePath = path.join.apply(path, [leading].concat(splitTrailing.slice(0, j + 1))),
  33. packageJsonPath = path.join(packagePath, 'package.json'),
  34. packageName = fs.existsSync(packageJsonPath) && require(packageJsonPath).name;
  35. // lookup any package name in the cache
  36. var linkedPackagePath = !!packageName && cache[packageName];
  37. if (linkedPackagePath) {
  38. // the remaining portion of the trailing path, not including the package path
  39. var remainingPath = path.join.apply(path, splitTrailing.slice(j + 1));
  40. // validate the remaining path in the linked location
  41. // failure implies we will keep trying nested sym-linked packages
  42. var linkedFilePath = path.join(linkedPackagePath, remainingPath),
  43. isValid = !!linkedFilePath && fs.existsSync(linkedFilePath) &&
  44. fs.statSync(linkedFilePath).isFile();
  45. // path is found where valid
  46. foundPath = isValid && linkedFilePath;
  47. }
  48. }
  49. // cache cannot be trusted if a file can't be found
  50. // set the cache to false to trigger its rebuild
  51. cache = !!foundPath && cache;
  52. }
  53. // the relative path should now be within the project
  54. return foundPath ? path.relative(from, foundPath) : relative;
  55. }
  56. }
  57. module.exports = enhancedRelative;
  58. /**
  59. * Make a hash of linked modules within the given directory by breadth-first search.
  60. * @param {string} directory A path to start searching
  61. * @returns {object} A collection of sym-linked paths within the project keyed by their package name
  62. */
  63. function indexLinkedModules(directory) {
  64. var buffer = listSymLinkedModules(directory),
  65. hash = {};
  66. // while there are items in the buffer
  67. while (buffer.length > 0) {
  68. var modulePath = buffer.shift(),
  69. packageJsonPath = path.join(modulePath, 'package.json'),
  70. packageName = fs.existsSync(packageJsonPath) && require(packageJsonPath).name;
  71. if (packageName) {
  72. // add this path keyed by package name, so long as it doesn't exist at a lower level
  73. hash[packageName] = hash[packageName] || modulePath;
  74. // detect nested module and push to the buffer (breadth-first)
  75. buffer.push.apply(buffer, listSymLinkedModules(modulePath));
  76. }
  77. }
  78. return hash;
  79. function listSymLinkedModules(directory) {
  80. var modulesPath = path.join(directory, 'node_modules'),
  81. hasNodeModules = fs.existsSync(modulesPath) && fs.statSync(modulesPath).isDirectory(),
  82. subdirectories = !!hasNodeModules && fs.readdirSync(modulesPath) || [];
  83. return subdirectories
  84. .map(joinDirectory)
  85. .filter(testIsSymLink);
  86. function joinDirectory(subdirectory) {
  87. return path.join(modulesPath, subdirectory);
  88. }
  89. function testIsSymLink(directory) {
  90. return fs.lstatSync(directory).isSymbolicLink(); // must use lstatSync not statSync
  91. }
  92. }
  93. }