sass-graph.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. 'use strict';
  2. var fs = require('fs');
  3. var path = require('path');
  4. var _ = require('lodash');
  5. var glob = require('glob');
  6. var parseImports = require('./parse-imports');
  7. // resolve a sass module to a path
  8. function resolveSassPath(sassPath, loadPaths, extensions) {
  9. // trim sass file extensions
  10. var re = new RegExp('(\.('+extensions.join('|')+'))$', 'i');
  11. var sassPathName = sassPath.replace(re, '');
  12. // check all load paths
  13. var i, j, length = loadPaths.length, scssPath, partialPath;
  14. for (i = 0; i < length; i++) {
  15. for (j = 0; j < extensions.length; j++) {
  16. scssPath = path.normalize(loadPaths[i] + '/' + sassPathName + '.' + extensions[j]);
  17. if (fs.existsSync(scssPath) && fs.lstatSync(scssPath).isFile()) {
  18. return scssPath;
  19. }
  20. }
  21. // special case for _partials
  22. for (j = 0; j < extensions.length; j++) {
  23. scssPath = path.normalize(loadPaths[i] + '/' + sassPathName + '.' + extensions[j]);
  24. partialPath = path.join(path.dirname(scssPath), '_' + path.basename(scssPath));
  25. if (fs.existsSync(partialPath) && fs.lstatSync(partialPath).isFile()) {
  26. return partialPath;
  27. }
  28. }
  29. }
  30. // File to import not found or unreadable so we assume this is a custom import
  31. return false;
  32. }
  33. function Graph(options, dir) {
  34. this.dir = dir;
  35. this.loadPaths = options.loadPaths || [];
  36. this.extensions = options.extensions || [];
  37. this.index = {};
  38. if (dir) {
  39. var graph = this;
  40. _.each(glob.sync(dir+'/**/*.@('+this.extensions.join('|')+')', { dot: true, nodir: true }), function(file) {
  41. graph.addFile(path.resolve(file));
  42. });
  43. }
  44. }
  45. // add a sass file to the graph
  46. Graph.prototype.addFile = function(filepath, parent) {
  47. var entry = this.index[filepath] = this.index[filepath] || {
  48. imports: [],
  49. importedBy: [],
  50. modified: fs.statSync(filepath).mtime
  51. };
  52. var resolvedParent;
  53. var imports = parseImports(fs.readFileSync(filepath, 'utf-8'));
  54. var cwd = path.dirname(filepath);
  55. var i, length = imports.length, loadPaths, resolved;
  56. for (i = 0; i < length; i++) {
  57. loadPaths = _([cwd, this.dir]).concat(this.loadPaths).filter().uniq().value();
  58. resolved = resolveSassPath(imports[i], loadPaths, this.extensions);
  59. if (!resolved) continue;
  60. // recurse into dependencies if not already enumerated
  61. if (!_.includes(entry.imports, resolved)) {
  62. entry.imports.push(resolved);
  63. this.addFile(fs.realpathSync(resolved), filepath);
  64. }
  65. }
  66. // add link back to parent
  67. if (parent) {
  68. resolvedParent = _(parent).intersection(this.loadPaths).value();
  69. if (resolvedParent) {
  70. resolvedParent = parent.substr(parent.indexOf(resolvedParent));
  71. } else {
  72. resolvedParent = parent;
  73. }
  74. entry.importedBy.push(resolvedParent);
  75. }
  76. };
  77. // visits all files that are ancestors of the provided file
  78. Graph.prototype.visitAncestors = function(filepath, callback) {
  79. this.visit(filepath, callback, function(err, node) {
  80. if (err || !node) return [];
  81. return node.importedBy;
  82. });
  83. };
  84. // visits all files that are descendents of the provided file
  85. Graph.prototype.visitDescendents = function(filepath, callback) {
  86. this.visit(filepath, callback, function(err, node) {
  87. if (err || !node) return [];
  88. return node.imports;
  89. });
  90. };
  91. // a generic visitor that uses an edgeCallback to find the edges to traverse for a node
  92. Graph.prototype.visit = function(filepath, callback, edgeCallback, visited) {
  93. filepath = fs.realpathSync(filepath);
  94. var visited = visited || [];
  95. if (!this.index.hasOwnProperty(filepath)) {
  96. edgeCallback('Graph doesn\'t contain ' + filepath, null);
  97. }
  98. var edges = edgeCallback(null, this.index[filepath]);
  99. var i, length = edges.length;
  100. for (i = 0; i < length; i++) {
  101. if (!_.includes(visited, edges[i])) {
  102. visited.push(edges[i]);
  103. callback(edges[i], this.index[edges[i]]);
  104. this.visit(edges[i], callback, edgeCallback, visited);
  105. }
  106. }
  107. };
  108. function processOptions(options) {
  109. return _.assign({
  110. loadPaths: [process.cwd()],
  111. extensions: ['scss', 'css'],
  112. }, options);
  113. }
  114. module.exports.parseFile = function(filepath, options) {
  115. if (fs.lstatSync(filepath).isFile()) {
  116. filepath = path.resolve(filepath);
  117. options = processOptions(options);
  118. var graph = new Graph(options);
  119. graph.addFile(filepath);
  120. return graph;
  121. }
  122. // throws
  123. };
  124. module.exports.parseDir = function(dirpath, options) {
  125. if (fs.lstatSync(dirpath).isDirectory()) {
  126. dirpath = path.resolve(dirpath);
  127. options = processOptions(options);
  128. var graph = new Graph(options, dirpath);
  129. return graph;
  130. }
  131. // throws
  132. };