sass-graph.js 4.7KB

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