'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 };