You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
171 lines
5.1 KiB
171 lines
5.1 KiB
'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.exclude = options.exclude instanceof RegExp ? options.exclude : null; |
|
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) { |
|
try { |
|
graph.addFile(path.resolve(file)); |
|
} catch (e) {} |
|
}); |
|
} |
|
} |
|
|
|
// add a sass file to the graph |
|
Graph.prototype.addFile = function(filepath, parent) { |
|
if (this.exclude !== null && this.exclude.test(filepath)) return; |
|
|
|
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; |
|
|
|
// check exclcude regex |
|
if (this.exclude !== null && this.exclude.test(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; |
|
} |
|
|
|
// check exclcude regex |
|
if (!(this.exclude !== null && this.exclude.test(resolvedParent))) { |
|
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 Object.assign({ |
|
loadPaths: [process.cwd()], |
|
extensions: ['scss', '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 |
|
};
|
|
|