From ed52c7efac6c70c5a6fed4c4c583a8920b019e4a Mon Sep 17 00:00:00 2001 From: xzyfer Date: Mon, 4 May 2015 11:54:55 +1000 Subject: [PATCH 1/8] Refactor CLI --- bin/sassgraph | 139 +++++++++++++++++++++++++++++++------------------- package.json | 4 +- 2 files changed, 89 insertions(+), 54 deletions(-) diff --git a/bin/sassgraph b/bin/sassgraph index a181e61..af4f062 100755 --- a/bin/sassgraph +++ b/bin/sassgraph @@ -1,80 +1,115 @@ #!/usr/bin/env node +var fs = require('fs'); +var path = require('path'); -var path = require("path"); -var fs = require("fs"); -var program = require("commander"); - -var sassGraph = require("../sass-graph"); -var package = require("../package.json"); - -program.version("Sass Graph Version " + package.version); - -program - .usage([ - "[OPTIONS] DIR FILE", - program._version, - "DIR is a directory containing scss files. FILE is a scss file to analyze." - ].join("\n\n ")) - .option("-I ", "Add dir to the sass load path. Multiple dirs can be comma delimited.") - .option("-a", "Print ancestors") - .option("-d", "Print descendents") - .option("--json", "Prints the index in json") - .option("-v, --version", "Display version information") - .parse(process.argv); - -if ( ! program.args.length) { - program.help(); +var command, directory, file; + +var yargs = require('yargs') + .usage('Usage: $0 [options] [file]') + // .demand(1) + + .command('ancestors', 'Output the ancestors') + .command('descendents', 'Output the descendents') + + .example('$0 ancestors -I src src/ src/_footer.scss', 'outputs the ancestors of src/_footer.scss') + + .option('I', { + alias: 'load-path', + default: [process.cwd()], + describe: 'Add directories to the sass load path', + type: 'array', + }) + + .option('e', { + alias: 'extensions', + default: ['scss', 'css'], + describe: 'File extensions to include in the graph', + type: 'array', + }) + + .option('j', { + alias: 'json', + default: false, + describe: 'Output the index in json', + type: 'bool', + }) + + .version(function() { + return require('../package').version; + }) + .alias('v', 'version') + + .help('h') + .alias('h', 'help'); + +var argv = yargs.argv; + +if (argv._.length === 0) { + yargs.showHelp(); + process.exit(1); } -var loadPaths = []; +if (['ancestors', 'descendents'].indexOf(argv._[0]) !== -1) { + command = argv._.shift(); +} -// load paths are comma delimited -if(program.I) { - loadPaths = program.I.split(/,/).map(function(f){ - return path.resolve(f); - }); +if (argv._ && path.extname(argv._[0]) === '') { + directory = argv._.shift(); } -// SASS_PATH can contain a colon delimited list of paths -if(process.env.SASS_PATH) { - loadPaths = loadPaths.concat(process.env.SASS_PATH.split(/:/).map(function(f){ - return path.resolve(f); - })); +if (argv._ && path.extname(argv._[0])) { + file = argv._.shift(); } -var sassDir = program.args[0]; -var sassFile = program.args[1]; try { - if(!fs.lstatSync(sassDir).isDirectory()) { - console.error("%s must be a directory", sassDir); - process.exit(1); + if (!directory) { + throw new Error('Missing directory'); + } + + if (!command && !argv.json) { + throw new Error('Missing command'); } - var graph = sassGraph.parseDir(sassDir, { loadPaths: loadPaths }); + if (!file && (command === 'ancestors' || command === 'descendents')) { + throw new Error(command + ' command requires a file'); + } - if(program.json) { - console.log(JSON.stringify(graph.index, null, 4)); - process.exit(0); + var loadPaths = argv.loadPath; + if(process.env.SASS_PATH) { + loadPaths = loadPaths.concat(process.env.SASS_PATH.split(/:/).map(function(f) { + return path.resolve(f); + })); } - if (program.A) { - graph.visitAncestors(path.resolve(sassFile), function(f) { - console.log(f); + var graph = require('../').parseDir(directory, { + loadPaths: loadPaths, }); + + if(argv.json) { + console.log(JSON.stringify(graph.index, null, 4)); + process.exit(0); } - if (program.D) { - graph.visitDescendents(path.resolve(sassFile), function(f) { + + if (command === 'ancestors') { + graph.visitAncestors(path.resolve(file), function(f) { console.log(f); }); } - if (program.B) { - graph.visitDescendents(path.resolve(sassFile), function(f) { + + if (command === 'descendents') { + graph.visitDescendents(path.resolve(file), function(f) { console.log(f); }); } } catch(e) { - console.log(e); + if (e.code === 'ENOENT') { + console.error('Error: no such file or directory "' + e.path + '"'); + } + else { + console.log('Error: ' + e.message); + } + // console.log(e.stack); process.exit(1); } diff --git a/package.json b/package.json index c5dc727..62102fe 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,9 @@ "graph" ], "dependencies": { - "commander": "^2.6.0", "glob": "^4.3.4", - "lodash": "^2.4.1" + "lodash": "^2.4.1", + "yargs": "^3.8.0" }, "devDependencies": { "assert": "^1.3.0", From bef22a047b49fe0c1e0a694a2f0e9dd021c950d4 Mon Sep 17 00:00:00 2001 From: xzyfer Date: Mon, 4 May 2015 11:57:34 +1000 Subject: [PATCH 2/8] Allow file extensions to be configurable --- bin/sassgraph | 3 ++- sass-graph.js | 51 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/bin/sassgraph b/bin/sassgraph index af4f062..393928a 100755 --- a/bin/sassgraph +++ b/bin/sassgraph @@ -83,8 +83,9 @@ try { } var graph = require('../').parseDir(directory, { + extensions: argv.extensions, loadPaths: loadPaths, - }); + }); if(argv.json) { console.log(JSON.stringify(graph.index, null, 4)); diff --git a/sass-graph.js b/sass-graph.js index cb5895c..b807d64 100644 --- a/sass-graph.js +++ b/sass-graph.js @@ -7,16 +7,17 @@ var glob = require('glob'); var parseImports = require('./parse-imports'); // resolve a sass module to a path -function resolveSassPath(sassPath, loadPaths) { +function resolveSassPath(sassPath, loadPaths, extensions) { // trim sass file extensions - var sassPathName = sassPath.replace(/\.(css|s[ca]ss)$/, ''); + var re = new RegExp('(\.('+extensions.join('|')+'))$', 'i'); + var sassPathName = sassPath.replace(re, ''); // check all load paths - var i, length = loadPaths.length, extensions = [".css", ".sass", ".scss"]; + var i, length = loadPaths.length; for(i = 0; i < length; i++) { var scssPath; for (var j = 0; j < extensions.length; j++) { - scssPath = path.normalize(loadPaths[i] + "/" + sassPathName + extensions[j]); + scssPath = path.normalize(loadPaths[i] + '/' + sassPathName + '.' + extensions[j]); if (fs.existsSync(scssPath)) { return scssPath; } @@ -26,8 +27,8 @@ function resolveSassPath(sassPath, loadPaths) { // special case for _partials for (var j = 0; j < extensions.length; j++) { - scssPath = path.normalize(loadPaths[i] + "/" + sassPathName + extensions[j]); - partialPath = path.join(path.dirname(scssPath), "_" + path.basename(scssPath)); + scssPath = path.normalize(loadPaths[i] + '/' + sassPathName + '.' + extensions[j]); + partialPath = path.join(path.dirname(scssPath), '_' + path.basename(scssPath)); if (fs.existsSync(partialPath)) { return partialPath; } @@ -35,17 +36,18 @@ function resolveSassPath(sassPath, loadPaths) { } // File to import not found or unreadable so we assume this is a custom import - return false + return false; } -function Graph(loadPaths, dir) { +function Graph(options, dir) { this.dir = dir; - this.loadPaths = loadPaths; + this.loadPaths = options.loadPaths || []; + this.extensions = options.extensions || []; this.index = {}; if(dir) { var graph = this; - _(glob.sync(dir+"/**/*.s[ca]ss", {})).forEach(function(file) { + _(glob.sync(dir+'/**/*.@('+this.extensions.join('|')+')', { dot: true })).forEach(function(file) { graph.addFile(path.resolve(file)); }); } @@ -117,7 +119,7 @@ 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); + edgeCallback('Graph doesn\'t contain ' + filepath, null); } var edges = edgeCallback(null, this.index[filepath]); @@ -133,21 +135,28 @@ Graph.prototype.visit = function(filepath, callback, edgeCallback, visited) { function processOptions(options) { options = options || {}; - if(!options.hasOwnProperty('loadPaths')) options['loadPaths'] = []; + options.loadPaths = !!(options.loadPaths) ? options.loadPaths : []; + options.extensions = !!(options.extensions) ? options.extensions : ['scss', 'css']; return options; } module.exports.parseFile = function(filepath, options) { - var filepath = path.resolve(filepath); - var options = processOptions(options); - var graph = new Graph(options.loadPaths); - graph.addFile(filepath); - return graph; + 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) { - var dirpath = path.resolve(dirpath); - var options = processOptions(options); - var graph = new Graph(options.loadPaths, dirpath); - return graph; + if (fs.lstatSync(dirpath).isDirectory()) { + dirpath = path.resolve(dirpath); + options = processOptions(options); + var graph = new Graph(options, dirpath); + return graph; + } + // throws }; From b3321f6dc66aaae1ae1e70f060c70a1f1e54934d Mon Sep 17 00:00:00 2001 From: xzyfer Date: Mon, 4 May 2015 11:58:09 +1000 Subject: [PATCH 3/8] Prioritize cwd when resolving load paths --- sass-graph.js | 10 +++------- test/fixtures/i.scss | 1 + test/test.js | 41 ++++++++++++++++++++++++++--------------- 3 files changed, 30 insertions(+), 22 deletions(-) create mode 100644 test/fixtures/i.scss diff --git a/sass-graph.js b/sass-graph.js index b807d64..9f8ba8e 100644 --- a/sass-graph.js +++ b/sass-graph.js @@ -65,14 +65,10 @@ Graph.prototype.addFile = function(filepath, parent) { var imports = parseImports(fs.readFileSync(filepath, 'utf-8')); var cwd = path.dirname(filepath); - var i, length = imports.length; + var i, length = imports.length, loadPaths, resolved; for (i = 0; i < length; i++) { - [this.dir, cwd].forEach(function (path) { - if (path && this.loadPaths.indexOf(path) === -1) { - this.loadPaths.push(path); - } - }.bind(this)); - var resolved = resolveSassPath(imports[i], _.uniq(this.loadPaths)); + 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 diff --git a/test/fixtures/i.scss b/test/fixtures/i.scss new file mode 100644 index 0000000..6560533 --- /dev/null +++ b/test/fixtures/i.scss @@ -0,0 +1 @@ +.g { color: strawberry; } diff --git a/test/test.js b/test/test.js index 1298af8..8905689 100644 --- a/test/test.js +++ b/test/test.js @@ -4,24 +4,25 @@ var path = require("path"); var fixtures = path.resolve("test/fixtures"); var files = { - 'a.scss': fixtures + "/a.scss", - 'b.scss': fixtures + "/b.scss", - '_c.scss': fixtures + "/_c.scss", - 'd.scss': fixtures + "/d.scss", - '_e.scss': fixtures + "/components/_e.scss", - 'f.scss': fixtures + "/f.scss", - 'g.scss': fixtures + "/g.scss", - '_h.scss': fixtures + "/nested/_h.scss", - '_i.scss': fixtures + "/nested/_i.scss", - 'j.scss': fixtures + "/j.scss", - 'k.l.scss': fixtures + "/components/k.l.scss", - 'm.scss': fixtures + "/m.scss", - '_n.scss': fixtures + "/compass/_n.scss", - '_compass.scss': fixtures + "/components/_compass.scss" + 'a.scss': fixtures + '/a.scss', + 'b.scss': fixtures + '/b.scss', + '_c.scss': fixtures + '/_c.scss', + 'd.scss': fixtures + '/d.scss', + '_e.scss': fixtures + '/components/_e.scss', + 'f.scss': fixtures + '/f.scss', + 'g.scss': fixtures + '/g.scss', + '_h.scss': fixtures + '/nested/_h.scss', + '_i.scss': fixtures + '/nested/_i.scss', + 'i.scss': fixtures + '/_i.scss', + 'j.scss': fixtures + '/j.scss', + 'k.l.scss': fixtures + '/components/k.l.scss', + 'm.scss': fixtures + '/m.scss', + '_n.scss': fixtures + '/compass/_n.scss', + '_compass.scss': fixtures + '/components/_compass.scss' } describe('sass-graph', function(){ - var sassGraph = require("../sass-graph"); + var sassGraph = require('../sass-graph'); describe('parsing a graph of all scss files', function(){ var graph = sassGraph.parseDir(fixtures, {loadPaths: [fixtures + '/components']}); @@ -59,6 +60,16 @@ describe('sass-graph', function(){ }) assert.deepEqual([files['b.scss'], files['a.scss']], ancestors); }); + + it('should prioritize cwd', function() { + var expectedDescendents = [files['_i.scss']]; + var descendents = []; + + graph.visitDescendents(files['_h.scss'], function (imp) { + descendents.push(imp); + assert.notEqual(expectedDescendents.indexOf(imp), -1); + }); + }); }) describe('parseFile', function () { From 1647dd754ac77132c003fc1540f610aee008b09e Mon Sep 17 00:00:00 2001 From: xzyfer Date: Mon, 4 May 2015 12:03:05 +1000 Subject: [PATCH 4/8] Update changelog for 2.0.0 --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89e10c2..55ff3d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,19 @@ All notable changes to this project will be documented in this file. ### Tests +## [2.0.0] +### BREAKING CHANGES +- `.sass` files are not included in the graph by default. Use the `-e .sass` flag. + +### Features +- Configurable file extensions - [@dannymidnight](https://github.com/dannymidnight), [@xzyfer](https://github.com/xzyfer) + +### Fixed +- Prioritize cwd when resolving load paths - [@schnerd](https://github.com/schnerd) + +### Tests +- Added test for prioritizing cwd when resolving load paths - [@xzyfer](https://github.com/xzyfer) + ## [1.3.0] ### Features - Add support for indented syntax - [@vegetableman](https://github.com/vegetableman) From 2d240dedbb92b1441ecca5e93ea9d03bf092ccd3 Mon Sep 17 00:00:00 2001 From: xzyfer Date: Mon, 4 May 2015 12:07:13 +1000 Subject: [PATCH 5/8] Update readme for 2.0.0 --- readme.md | 91 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 23 deletions(-) diff --git a/readme.md b/readme.md index 94aaea8..ff4cf58 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # Sass Graph -Parses sass and exposes a graph of dependencies +Parses Sass files in a directory and exposes a graph of dependencies [![Build Status](https://travis-ci.org/xzyfer/sass-graph.svg?branch=master)](https://travis-ci.org/xzyfer/sass-graph) [![npm version](https://badge.fury.io/js/sass-graph.svg)](http://badge.fury.io/js/sass-graph) @@ -20,24 +20,7 @@ npm install --save-dev sass-graph Usage as a Node library: ```js -$ node -> var sassGraph = require('./sass-graph'); -undefined -> sassGraph.parseDir('tests/fixtures'); -{ index: {, - 'tests/fixtures/a.scss': { - imports: ['b.scss'], - importedBy: [], - }, - 'tests/fixtures/b.scss': { - imports: ['_c.scss'], - importedBy: ['a.scss'], - }, - 'tests/fixtures/_c.scss': { - imports: [], - importedBy: ['b/scss'], - }, -}} +var sassGraph = require('./sass-graph'); ``` Usage as a command line tool: @@ -45,10 +28,72 @@ Usage as a command line tool: The command line tool will parse a graph and then either display ancestors, descendents or both. ``` -$ ./bin/sassgraph tests/fixtures tests/fixtures/a.scss -d -tests/fixtures/a.scss -tests/fixtures/b.scss -tests/fixtures/_c.scss +$ ./bin/sassgraph --help +Usage: bin/sassgraph [options] [file] + +Commands: + ancestors Output the ancestors + descendents Output the descendents + +Options: + -I, --load-path Add directories to the sass load path + -e, --extensions File extensions to include in the graph + -j, --json Output the index in json + -h, --help Show help + -v, --version Show version number + +Examples: + ./bin/sassgraph descendents test/fixtures test/fixtures/a.scss + /path/to/test/fixtures/b.scss + /path/to/test/fixtures/_c.scss +``` + +## API + +#### parseDir + +Parses a directory and builds a dependency graph of all requested file extensions. + +#### parseFile + +Parses a file and builds its dependency graph. + +## Options + +#### loadPaths + +Type: `Array` +Default: `[process.cwd]` + +Directories to use when resolved `@import` directives. + +#### extensions + +Type: `Array` +Default: `['scss', 'css']` + +File types to be parsed. + +## Example + +```js +var sassGraph = require('./sass-graph'); +console.log(sassGraph.parseDir('test/fixtures')); + +//{ index: {, +// '/path/to/test/fixtures/a.scss': { +// imports: ['b.scss'], +// importedBy: [], +// }, +// '/path/to/test/fixtures/b.scss': { +// imports: ['_c.scss'], +// importedBy: ['a.scss'], +// }, +// '/path/to/test/fixtures/_c.scss': { +// imports: [], +// importedBy: ['b/scss'], +// }, +//}} ``` ## Running Mocha tests From f79387a3cb11aea1522a3b119e5c553371e75947 Mon Sep 17 00:00:00 2001 From: xzyfer Date: Mon, 4 May 2015 13:24:42 +1000 Subject: [PATCH 6/8] Update glob@^5.0.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 62102fe..4724107 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "graph" ], "dependencies": { - "glob": "^4.3.4", + "glob": "^5.0.5", "lodash": "^2.4.1", "yargs": "^3.8.0" }, From 6c9c996e4f55476cde360f944682af41d83badff Mon Sep 17 00:00:00 2001 From: xzyfer Date: Mon, 4 May 2015 13:43:25 +1000 Subject: [PATCH 7/8] Update lodash@^3.8.0 --- package.json | 2 +- sass-graph.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4724107..c995171 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ ], "dependencies": { "glob": "^5.0.5", - "lodash": "^2.4.1", + "lodash": "^3.8.0", "yargs": "^3.8.0" }, "devDependencies": { diff --git a/sass-graph.js b/sass-graph.js index 9f8ba8e..1152dfc 100644 --- a/sass-graph.js +++ b/sass-graph.js @@ -49,7 +49,7 @@ function Graph(options, dir) { var graph = this; _(glob.sync(dir+'/**/*.@('+this.extensions.join('|')+')', { dot: true })).forEach(function(file) { graph.addFile(path.resolve(file)); - }); + }).value(); } } From 410c85d540cb1d35948c1e0ed149aba93fee8c71 Mon Sep 17 00:00:00 2001 From: xzyfer Date: Mon, 4 May 2015 15:22:27 +1000 Subject: [PATCH 8/8] Minor cleanup --- sass-graph.js | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/sass-graph.js b/sass-graph.js index 1152dfc..6b53890 100644 --- a/sass-graph.js +++ b/sass-graph.js @@ -12,21 +12,17 @@ function resolveSassPath(sassPath, loadPaths, extensions) { var re = new RegExp('(\.('+extensions.join('|')+'))$', 'i'); var sassPathName = sassPath.replace(re, ''); // check all load paths - var i, length = loadPaths.length; - for(i = 0; i < length; i++) { - var scssPath; - - for (var j = 0; j < extensions.length; j++) { + 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]); if (fs.existsSync(scssPath)) { return scssPath; } } - var partialPath; - // special case for _partials - for (var j = 0; j < extensions.length; j++) { + for (j = 0; j < extensions.length; j++) { scssPath = path.normalize(loadPaths[i] + '/' + sassPathName + '.' + extensions[j]); partialPath = path.join(path.dirname(scssPath), '_' + path.basename(scssPath)); if (fs.existsSync(partialPath)) { @@ -45,7 +41,7 @@ function Graph(options, dir) { this.extensions = options.extensions || []; this.index = {}; - if(dir) { + if (dir) { var graph = this; _(glob.sync(dir+'/**/*.@('+this.extensions.join('|')+')', { dot: true })).forEach(function(file) { graph.addFile(path.resolve(file)); @@ -72,20 +68,18 @@ Graph.prototype.addFile = function(filepath, parent) { if (!resolved) continue; // recurse into dependencies if not already enumerated - if(!_.contains(entry.imports, resolved)) { + if (!_.contains(entry.imports, resolved)) { entry.imports.push(resolved); this.addFile(fs.realpathSync(resolved), filepath); } } // add link back to parent - if(parent) { - resolvedParent = _.find(this.loadPaths, function(path) { - return parent.indexOf(path) !== -1; - }); + if (parent) { + resolvedParent = _(parent).intersection(this.loadPaths).value(); if (resolvedParent) { - resolvedParent = parent.substr(parent.indexOf(resolvedParent));//.replace(/^\/*/, ''); + resolvedParent = parent.substr(parent.indexOf(resolvedParent)); } else { resolvedParent = parent; } @@ -114,14 +108,14 @@ Graph.prototype.visitDescendents = function(filepath, callback) { Graph.prototype.visit = function(filepath, callback, edgeCallback, visited) { filepath = fs.realpathSync(filepath); var visited = visited || []; - if(!this.index.hasOwnProperty(filepath)) { + 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(!_.contains(visited, edges[i])) { + if (!_.contains(visited, edges[i])) { visited.push(edges[i]); callback(edges[i], this.index[edges[i]]); this.visit(edges[i], callback, edgeCallback, visited); @@ -130,10 +124,10 @@ Graph.prototype.visit = function(filepath, callback, edgeCallback, visited) { }; function processOptions(options) { - options = options || {}; - options.loadPaths = !!(options.loadPaths) ? options.loadPaths : []; - options.extensions = !!(options.extensions) ? options.extensions : ['scss', 'css']; - return options; + return _.assign({ + loadPaths: [process.cwd()], + extensions: ['scss', 'css'], + }, options); } module.exports.parseFile = function(filepath, options) {