| 'use strict'; |
| |
| var fs = require('fs'); |
| var path = require('path'); |
| var _ = require('lodash'); |
| var glob = require('glob'); |
| var parseImports = require('./parse-imports'); |
| |
| // resolve a sass module to a path |
| function resolveSassPath(sassPath, loadPaths, extensions) { |
| // trim sass file extensions |
| var re = new RegExp('(\.('+extensions.join('|')+'))$', 'i'); |
| var sassPathName = sassPath.replace(re, ''); |
| // check all load paths |
| var i, j, length = loadPaths.length, scssPath, partialPath; |
| for (i = 0; i < length; i++) { |
| for (j = 0; j < extensions.length; j++) { |
| scssPath = path.normalize(loadPaths[i] + '/' + sassPathName + '.' + extensions[j]); |
| try { |
| if (fs.lstatSync(scssPath).isFile()) { |
| return scssPath; |
| } |
| } catch (e) {} |
| } |
| |
| // special case for _partials |
| for (j = 0; j < extensions.length; j++) { |
| scssPath = path.normalize(loadPaths[i] + '/' + sassPathName + '.' + extensions[j]); |
| partialPath = path.join(path.dirname(scssPath), '_' + path.basename(scssPath)); |
| try { |
| if (fs.lstatSync(partialPath).isFile()) { |
| return partialPath; |
| } |
| } catch (e) {} |
| } |
| } |
| |
| // File to import not found or unreadable so we assume this is a custom import |
| return false; |
| } |
| |
| function Graph(options, dir) { |
| this.dir = dir; |
| this.extensions = options.extensions || []; |
| this.index = {}; |
| this.follow = options.follow || false; |
| this.loadPaths = _(options.loadPaths).map(function(p) { |
| return path.resolve(p); |
| }).value(); |
| |
| if (dir) { |
| var graph = this; |
| _.each(glob.sync(dir+'/**/*.@('+this.extensions.join('|')+')', { dot: true, nodir: true, follow: this.follow }), function(file) { |
| graph.addFile(path.resolve(file)); |
| }); |
| } |
| } |
| |
| // add a sass file to the graph |
| Graph.prototype.addFile = function(filepath, parent) { |
| var entry = this.index[filepath] = this.index[filepath] || { |
| imports: [], |
| importedBy: [], |
| modified: fs.statSync(filepath).mtime |
| }; |
| |
| var resolvedParent; |
| var isIndentedSyntax = path.extname(filepath) === '.sass'; |
| var imports = parseImports(fs.readFileSync(filepath, 'utf-8'), isIndentedSyntax); |
| var cwd = path.dirname(filepath); |
| |
| var i, length = imports.length, loadPaths, resolved; |
| for (i = 0; i < length; i++) { |
| loadPaths = _([cwd, this.dir]).concat(this.loadPaths).filter().uniq().value(); |
| resolved = resolveSassPath(imports[i], loadPaths, this.extensions); |
| if (!resolved) continue; |
| |
| // recurse into dependencies if not already enumerated |
| if (!_.includes(entry.imports, resolved)) { |
| entry.imports.push(resolved); |
| this.addFile(fs.realpathSync(resolved), filepath); |
| } |
| } |
| |
| // add link back to parent |
| if (parent) { |
| resolvedParent = _(parent).intersection(this.loadPaths).value(); |
| |
| if (resolvedParent) { |
| resolvedParent = parent.substr(parent.indexOf(resolvedParent)); |
| } else { |
| resolvedParent = parent; |
| } |
| |
| entry.importedBy.push(resolvedParent); |
| } |
| }; |
| |
| // visits all files that are ancestors of the provided file |
| Graph.prototype.visitAncestors = function(filepath, callback) { |
| this.visit(filepath, callback, function(err, node) { |
| if (err || !node) return []; |
| return node.importedBy; |
| }); |
| }; |
| |
| // visits all files that are descendents of the provided file |
| Graph.prototype.visitDescendents = function(filepath, callback) { |
| this.visit(filepath, callback, function(err, node) { |
| if (err || !node) return []; |
| return node.imports; |
| }); |
| }; |
| |
| // a generic visitor that uses an edgeCallback to find the edges to traverse for a node |
| Graph.prototype.visit = function(filepath, callback, edgeCallback, visited) { |
| filepath = fs.realpathSync(filepath); |
| var visited = visited || []; |
| if (!this.index.hasOwnProperty(filepath)) { |
| edgeCallback('Graph doesn\'t contain ' + filepath, null); |
| } |
| var edges = edgeCallback(null, this.index[filepath]); |
| |
| var i, length = edges.length; |
| for (i = 0; i < length; i++) { |
| if (!_.includes(visited, edges[i])) { |
| visited.push(edges[i]); |
| callback(edges[i], this.index[edges[i]]); |
| this.visit(edges[i], callback, edgeCallback, visited); |
| } |
| } |
| }; |
| |
| function processOptions(options) { |
| return _.assign({ |
| loadPaths: [process.cwd()], |
| extensions: ['scss', 'css', 'sass'], |
| }, options); |
| } |
| |
| module.exports.parseFile = function(filepath, options) { |
| if (fs.lstatSync(filepath).isFile()) { |
| filepath = path.resolve(filepath); |
| options = processOptions(options); |
| var graph = new Graph(options); |
| graph.addFile(filepath); |
| return graph; |
| } |
| // throws |
| }; |
| |
| module.exports.parseDir = function(dirpath, options) { |
| if (fs.lstatSync(dirpath).isDirectory()) { |
| dirpath = path.resolve(dirpath); |
| options = processOptions(options); |
| var graph = new Graph(options, dirpath); |
| return graph; |
| } |
| // throws |
| }; |