├── fixtures ├── base │ ├── index.styl │ └── glob │ │ ├── styles1.styl │ │ └── styles2.styl ├── imports │ ├── bar.less │ ├── bar.proto │ ├── bar.styl │ ├── baz.less │ ├── baz.proto │ ├── qux.less │ ├── foo.styl │ ├── foo.less │ ├── foo.proto │ ├── foo-options.less │ └── foo-url.less ├── globbing.styl ├── recursive.jade ├── altExtensions.jade ├── module.styl ├── excludedDependencies.jade └── multilineImport.scss ├── .gitignore ├── .travis.yml ├── .editorconfig ├── package.json ├── LICENSE ├── README.md ├── index.js └── test.js /fixtures/base/index.styl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/imports/bar.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/imports/bar.proto: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/imports/bar.styl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/imports/baz.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/imports/baz.proto: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/imports/qux.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/base/glob/styles1.styl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/base/glob/styles2.styl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/globbing.styl: -------------------------------------------------------------------------------- 1 | @import 'base/glob/*' 2 | -------------------------------------------------------------------------------- /fixtures/imports/foo.styl: -------------------------------------------------------------------------------- 1 | @import 'bar.styl' 2 | -------------------------------------------------------------------------------- /fixtures/imports/foo.less: -------------------------------------------------------------------------------- 1 | @import "bar.less"; 2 | -------------------------------------------------------------------------------- /fixtures/recursive.jade: -------------------------------------------------------------------------------- 1 | include altExtensions 2 | -------------------------------------------------------------------------------- /fixtures/altExtensions.jade: -------------------------------------------------------------------------------- 1 | include htmlPartial.html 2 | -------------------------------------------------------------------------------- /fixtures/module.styl: -------------------------------------------------------------------------------- 1 | @import 'base' 2 | @import 'foo.styl' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | coverage 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "5" 5 | - "4" 6 | -------------------------------------------------------------------------------- /fixtures/imports/foo.proto: -------------------------------------------------------------------------------- 1 | import "bar.proto"; 2 | import public "baz.proto"; 3 | -------------------------------------------------------------------------------- /fixtures/imports/foo-options.less: -------------------------------------------------------------------------------- 1 | @import (multiple) "bar.less"; 2 | @import (less, reference) "baz.less"; 3 | -------------------------------------------------------------------------------- /fixtures/imports/foo-url.less: -------------------------------------------------------------------------------- 1 | @import url("bar.less"); 2 | @import url('baz.less'); 3 | @import url(qux.less); 4 | -------------------------------------------------------------------------------- /fixtures/excludedDependencies.jade: -------------------------------------------------------------------------------- 1 | include excludedDependencyOne 2 | include excludedDependencyTwo 3 | include includedDependencyOne -------------------------------------------------------------------------------- /fixtures/multilineImport.scss: -------------------------------------------------------------------------------- 1 | @import 2 | 'foo', 3 | 'bar', 'baz'; 4 | 5 | @import 'yep' 6 | 7 | body { 8 | border: 1; 9 | } 10 | 11 | @import 'compass/something' 12 | 13 | @import 'blerp' 14 | ,'beep'; 15 | 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "progeny", 3 | "version": "0.12.0", 4 | "description": "Recursively finds dependencies of style and template source files", 5 | "author": { 6 | "name": "Elan Shanker", 7 | "url": "http://github.com/es128" 8 | }, 9 | "homepage": "https://github.com/es128/progeny", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/es128/progeny" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/es128/progeny/issues" 17 | }, 18 | "scripts": { 19 | "test": "mocha" 20 | }, 21 | "dependencies": { 22 | "async-each": "^1.0.0", 23 | "chalk": "^1.1.3", 24 | "fs-mode": "^1.0.1", 25 | "glob": "^7.0.3" 26 | }, 27 | "devDependencies": { 28 | "mocha": "^2.4.5", 29 | "sinon": "^1.17.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Elan Shanker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | progeny 2 | ======= 3 | Recursively finds dependencies of style and template source files. 4 | 5 | Or configure it to do the same kind of thing with any other type of code file 6 | that has an `import`-type syntax. 7 | 8 | 9 | Usage 10 | ----- 11 | #### progeny ([config]) (path, [sourceContents], callback) 12 | #### progeny.Sync ([config]) (path, [sourceContents]) 13 | Call **progeny** with an optional configuration object, it returns a reusable 14 | function. Call that function with a path to a source file (and its 15 | source code if you already have it handy), and it will figure out all of that 16 | file's dependencies and sub-dependencies, passing an array of them to your 17 | callback. Or use the `Sync` API to get the results as a return value. 18 | 19 | Result array has non-enumerable `patterns` property with an array of 20 | glob patterns found in source files' `import` statements. 21 | 22 | Examples using `path` assume you already have `var path = require('path');`. 23 | You _could_ just use strings like `'/path/to/project'`, but you may run into 24 | cross-compatibility issues. 25 | 26 | ##### Quick and Simple 27 | You can skip the config object and the source code, letting **Progeny** read 28 | the source from the file itself and apply a built-in configuration based on the file extension. 29 | 30 | ```javascript 31 | var progeny = require('progeny'); 32 | var filePath = path.join('path', 'to', 'project', 'style-or-template.pug'); 33 | 34 | // Async 35 | progeny()(filePath, function (err, dependencies) { 36 | // use the dependencies array in here 37 | }); 38 | 39 | // Sync 40 | var dependencies = progeny()(filePath); 41 | ``` 42 | 43 | ##### Configuration 44 | There are 45 | [built-in configurations](https://github.com/es128/progeny/blob/master/index.js#L11-L52) 46 | already for `css`, `sass`/`scss`, `less`, `stylus`, `pug`/`jade`, `slm`, and `proto`. 47 | Configuration must be specified for any other formats. Feel free to submit Pull 48 | Requests to add default types, or improve the settings for the existing ones. 49 | 50 | ```javascript 51 | var progenyConfig = { 52 | // The file extension for the source code you want parsed 53 | // Will be derived from the source file path if not specified 54 | extension: 'styl', 55 | 56 | // Array of multiple file extensions to try when looking for dependencies 57 | extensionsList: ['scss', 'sass'], 58 | 59 | // Regexp to run on each line of source code to match dependency references 60 | // Make sure you wrap the file name part in (parentheses) 61 | regexp: /^\s*@import\s+['"]?([^'"]+)['"]?/, 62 | 63 | // File prefix to try (in addition to the raw value matched in the regexp) 64 | prefix: '_', 65 | 66 | // Matched stuff to exclude: string, regex, or array of either/both 67 | exclusion: /^compass/, 68 | 69 | // In case a match starts with a slash, the absolute path to apply 70 | rootPath: path.join('path', 'to', 'project'), 71 | 72 | // Other paths to check for possible dependency resolution 73 | altPaths: [ 74 | path.join('path', 'to', 'shared'), 75 | path.join('path', 'to', 'common') 76 | ], 77 | 78 | // An array of regexps to run in series for more complex dependency parsing 79 | // Useful for matching multiple dependencies from one, possibly mult-line, 80 | // statement. All regexps except the last one must use the global flag. 81 | multipass: [ 82 | /@import[^;]+;/g, 83 | /\s*['"][^'"]+['"]\s*,?/g, 84 | /(?:['"])([^'"]+)/ 85 | ], 86 | 87 | // By default the list of paths progeny provides will be limited to files 88 | // actually found in the file system. Use this option to get every possible 89 | // path progeny thinks a depencency could be located at. 90 | potentialDeps: true, 91 | 92 | // By default progeny will strip all line comments like "// ..." and 93 | // multiline comments like "/* ... */". You could disable this behavior. 94 | skipComments: true; 95 | 96 | // Custom resolver allows to preprocess dependency name. For example, webpack 97 | // has well known way to reference node_modules-related path with ~ in the 98 | // beginning of import name. 99 | // Return true, undefined or null to accept dependency without changes. 100 | // Return false to reject dependency. 101 | // Return new filename to override dependency. 102 | resolver: function (depFilename, parentDir, parentFilename) { 103 | if (depFilename.startsWith('~')) { 104 | var absPath = path.resolve(path.join('.', 'node_modules', depFilename.substr(1))); 105 | return path.relative(parentDir, absPath); 106 | } 107 | }, 108 | 109 | // Print dependency resolution information to console.log 110 | debug: true 111 | }; 112 | ``` 113 | 114 | ##### More Examples 115 | Process a list of files: 116 | 117 | ```javascript 118 | var progeny = require('progeny'); 119 | var getDependencies = progeny(progenyConfig); 120 | myFiles.forEach(function (file) { 121 | getDependencies(file.path, file.source, function (err, deps) { 122 | if (err) throw new Error(err); 123 | file.dependencies = deps; 124 | }); 125 | }); 126 | ``` 127 | 128 | Multiple configs: 129 | 130 | ```javascript 131 | var getDefaultDependencies = progeny(); 132 | var getCustomDependencies = progeny({ 133 | extension: 'foo', 134 | regexp: /([^\s,]+)/ 135 | }); 136 | ``` 137 | 138 | Process source code from a string without its file path: 139 | 140 | ```javascript 141 | var mySourceString; // assume this contains valid source code 142 | progeny({ 143 | // extension and rootPath must be specified for this to work 144 | // also need regexp if extension not one of the predefined ones 145 | extension: 'less', 146 | rootPath: path.join('path', 'to', 'project') 147 | })(null, mySourceString, function (err, deps) {}); 148 | ``` 149 | 150 | Change Log 151 | ---------- 152 | [See release notes page on GitHub](https://github.com/es128/progeny/releases) 153 | 154 | License 155 | ------- 156 | [MIT](https://raw.github.com/es128/progeny/master/LICENSE) 157 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sysPath = require('path'); 4 | var fs = require('fs-mode'); 5 | var each = require('async-each'); 6 | var glob = require('glob'); 7 | var chalk = require('chalk'); 8 | 9 | function defaultSettings(extName) { 10 | switch (extName) { 11 | case 'pug': 12 | case 'jade': 13 | return { 14 | regexp: /^\s*(?:include|extends)\s+(.+)/ 15 | }; 16 | case 'slm': 17 | return { 18 | regexp: /^\s*(?:=\s+include|=\s+content|=\s+partial)\s+(.+)/ 19 | }; 20 | case 'proto': 21 | return { 22 | regexp: /^\s*import\s+(?:public\s+)?"(.+)";/ 23 | }; 24 | case 'styl': 25 | return { 26 | regexp: /^\s*(?:@import|@require)\s*['"]?([^'"]+)['"]?/, 27 | exclusion: 'nib', 28 | moduleDep: true, 29 | globDeps: true 30 | }; 31 | case 'less': 32 | return { 33 | regexp: /^\s*@import\s*(?:\([\w, ]+\)\s*)?(?:(?:url\()?['"]?([^'")]+)['"]?)/ 34 | }; 35 | case 'sass': 36 | case 'scss': 37 | return { 38 | regexp: /^\s*@import\s*['"]?([^'"]+)['"]?/, 39 | prefix: '_', 40 | exclusion: /^compass/, 41 | extensionsList: ['scss', 'sass'], 42 | multipass: [ 43 | /@import[^;]+;/g, 44 | /\s*['"][^'"]+['"]\s*,?/g, 45 | /(?:['"])([^'"]+)/ 46 | ] 47 | }; 48 | case 'css': 49 | return { 50 | regexp: /^\s*@import\s*(?:url\()?['"]([^'"]+)['"]/, 51 | globDeps: true 52 | }; 53 | } 54 | 55 | return {}; 56 | } 57 | 58 | function printDepsList(path, depsList) { 59 | var formatted = depsList.map(function (p) { 60 | return ' |--' + sysPath.relative('.', p); 61 | }).join('\n'); 62 | 63 | console.log(chalk.green.bold('DEP') + ' ' + sysPath.relative('.', path)); 64 | console.log(formatted || ' | NO-DEP'); 65 | } 66 | 67 | function progenyConstructor(mode, settings) { 68 | settings = settings || {}; 69 | var rootPath = settings.rootPath; 70 | var altPaths = settings.altPaths; 71 | var extension = settings.extension; 72 | var regexp = settings.regexp; 73 | var prefix = settings.prefix; 74 | var exclusion = settings.exclusion; 75 | var extensionsList = settings.extensionsList; 76 | var multipass = settings.multipass; 77 | var potentialDeps = settings.potentialDeps; 78 | var moduleDep = settings.moduleDep; 79 | var globDeps = settings.globDeps; 80 | var reverseArgs = settings.reverseArgs; 81 | var debug = settings.debug; 82 | var resolver = settings.resolver; 83 | var skipComments = settings.skipComments; 84 | 85 | function parseDeps(path, source, depsList, callback) { 86 | var parent; 87 | if (path) { 88 | parent = sysPath.dirname(path); 89 | } 90 | 91 | if (skipComments) { 92 | source = source 93 | .replace(/\/\/[^\r\n]*(\r\n|\r|\n|$)/g, '$1') 94 | .replace(/\/\*[\s\S]*?\*\//g, ''); 95 | } 96 | 97 | var globs = []; 98 | var mdeps = []; 99 | if (multipass) { 100 | mdeps = multipass.slice(0, -1) 101 | .reduce(function (vals, regex) { 102 | return vals.map(function (val) { 103 | return val && val.match(regex) || []; 104 | }).reduce(function (flat, val) { 105 | return flat.concat(val); 106 | }, []); 107 | }, [source]) 108 | .map(function (val) { 109 | var last = multipass[multipass.length - 1]; 110 | return val.match(last)[1]; 111 | }); 112 | } 113 | 114 | var paths = source.toString() 115 | .split('\n') 116 | .map(function (line) { 117 | return line.match(regexp); 118 | }) 119 | .filter(function (match) { 120 | return match && match.length > 1; 121 | }) 122 | .map(function (match) { 123 | return match[1].trim(); 124 | }) 125 | .concat(mdeps) 126 | .filter(function (path) { 127 | return path && ![].concat(exclusion).some(function (ex) { 128 | if (ex instanceof RegExp) return ex.test(path); 129 | if (typeof ex === 'string') return ex === path; 130 | }); 131 | }) 132 | .map(function (path) { 133 | var isGlob = globDeps && glob.hasMagic(path); 134 | if (isGlob) globs.push(path); 135 | 136 | var allowExtendedImports = isGlob || moduleDep; 137 | if (!allowExtendedImports && extension && !sysPath.extname(path)) { 138 | return path + '.' + extension; 139 | } 140 | return path; 141 | }); 142 | 143 | var dirs = []; 144 | if (parent) { 145 | dirs.push(parent); 146 | } 147 | 148 | if (rootPath && rootPath !== parent) { 149 | dirs.push(rootPath); 150 | } 151 | 152 | if (Array.isArray(altPaths)) { 153 | [].push.apply(dirs, altPaths); 154 | } 155 | 156 | if (resolver) { 157 | paths = paths 158 | .map(function (filename) { 159 | var resolvedPath = resolver(filename, parent, path); 160 | if (resolvedPath === false) { 161 | return false; 162 | } 163 | if (resolvedPath === true 164 | || resolvedPath === null 165 | || resolvedPath === undefined 166 | ) { 167 | return filename; 168 | } 169 | return resolvedPath; 170 | }) 171 | .filter(function (path) { 172 | return path !== false; 173 | }); 174 | } 175 | 176 | var deps = []; 177 | dirs.forEach(function (dir) { 178 | globs.forEach(function (glob) { 179 | depsList.patterns.push(sysPath.join(dir, glob)); 180 | }); 181 | 182 | paths.forEach(function (path) { 183 | if (moduleDep && extension && !sysPath.extname(path)) { 184 | deps.push(sysPath.join(dir, path + '.' + extension)); 185 | deps.push(sysPath.join(dir, path, 'index.' + extension)); 186 | } else { 187 | deps.push(sysPath.join(dir, path)); 188 | } 189 | }); 190 | }); 191 | 192 | if (extension) { 193 | deps.forEach(function (path) { 194 | var isGlob = globDeps && glob.hasMagic(path); 195 | if (!isGlob && sysPath.extname(path) !== '.' + extension) { 196 | deps.push(path + '.' + extension); 197 | } 198 | }); 199 | } 200 | 201 | if (prefix) { 202 | var prefixed = []; 203 | deps.forEach(function (path) { 204 | var dir = sysPath.dirname(path); 205 | var file = sysPath.basename(path); 206 | if (file.indexOf(prefix) !== 0) { 207 | prefixed.push(sysPath.join(dir, prefix + file)); 208 | } 209 | }); 210 | [].push.apply(deps, prefixed); 211 | } 212 | 213 | if (extensionsList.length) { 214 | var altExts = []; 215 | deps.forEach(function (path) { 216 | var dir = sysPath.dirname(path); 217 | extensionsList.forEach(function (ext) { 218 | if (sysPath.extname(path) !== '.' + ext) { 219 | var base = sysPath.basename(path, '.' + extension); 220 | altExts.push(sysPath.join(dir, base + '.' + ext)); 221 | } 222 | }); 223 | }); 224 | 225 | [].push.apply(deps, altExts); 226 | } 227 | 228 | if (deps.length) { 229 | each(deps, function (path, callback) { 230 | if (depsList.indexOf(path) >= 0) { 231 | callback(); 232 | return; 233 | } 234 | if (globDeps && glob.hasMagic(path)) { 235 | var addDeps = function (files) { 236 | each(files, function (path, callback) { 237 | addDep(path, depsList, callback); 238 | }, callback); 239 | }; 240 | 241 | if (mode === 'Async') { 242 | glob(path, function (err, files) { 243 | if (err) { 244 | return callback(); 245 | } 246 | 247 | addDeps(files); 248 | }); 249 | } else { 250 | var files = glob.sync(path); 251 | addDeps(files); 252 | } 253 | } else { 254 | addDep(path, depsList, callback); 255 | } 256 | }, callback); 257 | } else { 258 | callback(); 259 | } 260 | } 261 | 262 | function addDep(path, depsList, callback) { 263 | if (depsList.indexOf(path) < 0) { 264 | depsList.push(path); 265 | } 266 | 267 | fs[mode].readFile(path, { encoding: 'utf8' }, function (err, source) { 268 | if (err) { 269 | if (!potentialDeps) { 270 | depsList.splice(depsList.indexOf(path), 1); 271 | } 272 | callback(); 273 | } else { 274 | parseDeps(path, source, depsList, callback); 275 | } 276 | }); 277 | } 278 | 279 | var progeny = function (path, source, callback) { 280 | if (typeof source === 'function') { 281 | callback = source; 282 | source = undefined; 283 | } 284 | 285 | if (source && typeof source === 'object' && 'path' in source) { 286 | path = source.path; 287 | source = source.data; 288 | } 289 | 290 | if (reverseArgs) { 291 | var temp = source; 292 | source = path; 293 | path = temp; 294 | } 295 | 296 | var depsList = []; 297 | Object.defineProperty(depsList, "patterns", { 298 | value: [], 299 | writable: true, 300 | configurable: true, 301 | }); 302 | 303 | extension = extension || sysPath.extname(path).slice(1); 304 | var def = defaultSettings(extension); 305 | if (regexp == null) regexp = def.regexp; 306 | if (prefix == null) prefix = def.prefix; 307 | if (exclusion == null) exclusion = def.exclusion; 308 | if (extensionsList == null) extensionsList = def.extensionsList || []; 309 | if (multipass == null) multipass = def.multipass; 310 | if (moduleDep == null) moduleDep = def.moduleDep; 311 | if (globDeps == null) globDeps = def.globDeps; 312 | if (debug == null) debug = def.debug; 313 | if (resolver == null) resolver = def.resolver; 314 | if (skipComments == null) skipComments = true; 315 | 316 | function run() { 317 | parseDeps(path, source, depsList, function () { 318 | if (debug) printDepsList(path, depsList); 319 | callback(null, depsList); 320 | }); 321 | } 322 | 323 | if (source) { 324 | run(); 325 | } else { 326 | fs[mode].readFile(path, { encoding: 'utf8' }, 327 | function (err, fileContents) { 328 | if (err) { 329 | return callback(err); 330 | } 331 | 332 | source = fileContents; 333 | run(); 334 | } 335 | ); 336 | } 337 | }; 338 | 339 | var progenySync = function (path, source) { 340 | var result = []; 341 | progeny(path, source, function (err, depsList) { 342 | if (err) { 343 | throw err; 344 | } 345 | 346 | result = depsList; 347 | }); 348 | 349 | return result; 350 | }; 351 | 352 | if (mode === 'Sync') { 353 | return progenySync; 354 | } 355 | 356 | return progeny; 357 | } 358 | 359 | 360 | module.exports = progenyConstructor.bind(null, 'Async'); 361 | module.exports.Sync = progenyConstructor.bind(null, 'Sync'); 362 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var progeny = require('./index'); 4 | var path = require('path'); 5 | var assert = require('assert'); 6 | var sinon = require('sinon'); 7 | 8 | function getFixturePath(subPath) { 9 | return path.join(__dirname, 'fixtures', subPath); 10 | } 11 | 12 | describe('progeny', function() { 13 | var o = { potentialDeps: true }; 14 | 15 | it('should preserve original file extensions', function (done) { 16 | progeny(o)(getFixturePath('altExtensions.jade'), function (err, dependencies) { 17 | var paths = [ 18 | getFixturePath('htmlPartial.html'), 19 | getFixturePath('htmlPartial.html.jade') 20 | ]; 21 | assert.deepEqual(dependencies, paths); 22 | done(); 23 | }); 24 | }); 25 | 26 | it('should resolve recursive dependencies', function (done) { 27 | progeny(o)(getFixturePath('recursive.jade'), function (err, dependencies) { 28 | var paths = [ 29 | getFixturePath('altExtensions.jade'), 30 | getFixturePath('htmlPartial.html'), 31 | getFixturePath('htmlPartial.html.jade') 32 | ]; 33 | assert.deepEqual(dependencies, paths); 34 | done(); 35 | }); 36 | }); 37 | 38 | it('should resolve module imports', function (done) { 39 | progeny(o)(getFixturePath('module.styl'), function (err, dependencies) { 40 | var paths = [ 41 | getFixturePath('base.styl'), 42 | getFixturePath('base/index.styl'), 43 | getFixturePath('foo.styl') 44 | ]; 45 | assert.deepEqual(dependencies, paths); 46 | done(); 47 | }); 48 | }); 49 | 50 | it('should resolve glob patterns', function (done) { 51 | progeny(o)(getFixturePath('globbing.styl'), function (err, dependencies) { 52 | var paths = [ 53 | getFixturePath('base/glob/styles1.styl'), 54 | getFixturePath('base/glob/styles2.styl') 55 | ]; 56 | 57 | assert.deepEqual(dependencies, paths); 58 | done(); 59 | }); 60 | }); 61 | 62 | it('should list found globs in .patterns', function (done) { 63 | progeny(o)(getFixturePath('globbing.styl'), function (err, dependencies) { 64 | assert.deepEqual(dependencies.patterns, [getFixturePath('base/glob/*')]); 65 | done(); 66 | }); 67 | }); 68 | 69 | it('should provide only real files by default', function (done) { 70 | progeny()(getFixturePath('recursive.jade'), function (err, dependencies) { 71 | var paths = [getFixturePath('altExtensions.jade')]; 72 | assert.deepEqual(dependencies, paths); 73 | done(); 74 | }); 75 | }); 76 | 77 | it('should resolve multiline @import statements', function (done) { 78 | progeny(o)(getFixturePath('multilineImport.scss'), function (err, dependencies) { 79 | // 6 non-excluded references in fixture 80 | // x4 for prefixed/unprefixed and both file extensions 81 | assert.equal(dependencies.length, 24); 82 | done(); 83 | }); 84 | }); 85 | 86 | it('should be truly async', function (done) { 87 | var dependencies = null; 88 | progeny(o)(getFixturePath('altExtensions.jade'), function (err, deps) { 89 | dependencies = deps; 90 | assert(Array.isArray, dependencies); 91 | done(); 92 | }); 93 | 94 | assert.equal(dependencies, null); 95 | }); 96 | 97 | it('should return empty array when there are no deps', function (done) { 98 | progeny(o)('foo.scss', '$a: 5px; .test {\n border-radius: $a; }\n', function (err, deps) { 99 | assert.deepEqual(deps, []); 100 | done(); 101 | }); 102 | }); 103 | 104 | it('should skip empty dependencies', function (done) { 105 | progeny(o)('foo.scss', '@import ""', function (err, deps) { 106 | assert.deepEqual(deps, []); 107 | done(); 108 | }); 109 | }); 110 | }); 111 | 112 | describe('progeny.Sync', function () { 113 | var o = { potentialDeps: true }; 114 | 115 | it('should return the result', function () { 116 | assert(Array.isArray(progeny.Sync()(getFixturePath('altExtensions.jade')))); 117 | }); 118 | 119 | it('should resolve glob patterns', function () { 120 | var dependencies = progeny.Sync(o)(getFixturePath('globbing.styl')); 121 | var paths = [ 122 | getFixturePath('base/glob/styles1.styl'), 123 | getFixturePath('base/glob/styles2.styl') 124 | ]; 125 | 126 | assert.deepEqual(dependencies, paths); 127 | }); 128 | }); 129 | 130 | describe('progeny configuration', function () { 131 | describe('excluded file list', function () { 132 | var progenyConfig; 133 | 134 | beforeEach(function () { 135 | progenyConfig = { 136 | rootPath: path.join(__dirname, 'fixtures'), 137 | exclusion: [ 138 | /excludedDependencyOne/, 139 | /excludedDependencyTwo/ 140 | ], 141 | extension: 'jade', 142 | potentialDeps: true 143 | }; 144 | }); 145 | 146 | it('should accept one regex', function (done) { 147 | progenyConfig.exclusion = /excludedDependencyOne/; 148 | var getDependencies = progeny(progenyConfig); 149 | 150 | getDependencies(getFixturePath('excludedDependencies.jade'), function (err, dependencies) { 151 | var paths = [ 152 | getFixturePath('excludedDependencyTwo.jade'), 153 | getFixturePath('includedDependencyOne.jade') 154 | ]; 155 | 156 | assert.deepEqual(dependencies, paths); 157 | done(); 158 | }); 159 | }); 160 | 161 | it('should accept one string', function (done) { 162 | progenyConfig.exclusion = 'excludedDependencyOne'; 163 | var getDependencies = progeny(progenyConfig); 164 | 165 | getDependencies(getFixturePath('excludedDependencies.jade'), function (err, dependencies) { 166 | var paths = [ 167 | getFixturePath('excludedDependencyTwo.jade'), 168 | getFixturePath('includedDependencyOne.jade') 169 | ]; 170 | 171 | assert.deepEqual(dependencies, paths); 172 | done(); 173 | }); 174 | }); 175 | 176 | it('should accept a list of regexes', function (done) { 177 | progenyConfig.exclusion = [ 178 | /excludedDependencyOne/, 179 | /excludedDependencyTwo/ 180 | ]; 181 | var getDependencies = progeny(progenyConfig); 182 | 183 | getDependencies(getFixturePath('excludedDependencies.jade'), function (err, dependencies) { 184 | assert.deepEqual(dependencies, [getFixturePath('includedDependencyOne.jade')]); 185 | done(); 186 | }); 187 | }); 188 | 189 | it('should accept a list of strings', function (done) { 190 | progenyConfig.exclusion = [ 191 | 'excludedDependencyOne', 192 | 'excludedDependencyTwo' 193 | ]; 194 | var getDependencies = progeny(progenyConfig); 195 | 196 | getDependencies(getFixturePath('excludedDependencies.jade'), function (err, dependencies) { 197 | assert.deepEqual(dependencies, [getFixturePath('includedDependencyOne.jade')]); 198 | done(); 199 | }); 200 | }); 201 | 202 | it('should accept a list of both strings and regexps', function (done) { 203 | progenyConfig.exclusion = [ 204 | 'excludedDependencyOne', 205 | /excludedDependencyTwo/ 206 | ] 207 | var getDependencies = progeny(progenyConfig); 208 | 209 | getDependencies(getFixturePath('excludedDependencies.jade'), function (err, dependencies) { 210 | assert.deepEqual(dependencies, [getFixturePath('includedDependencyOne.jade')]); 211 | done(); 212 | }); 213 | }); 214 | }); 215 | 216 | describe('altPaths', function () { 217 | it('should look for deps in altPaths', function (done) { 218 | var progenyConfig = { 219 | altPaths: [getFixturePath('subdir')], 220 | potentialDeps: true 221 | }; 222 | 223 | progeny(progenyConfig)(getFixturePath('altExtensions.jade'), function (err, dependencies) { 224 | var paths = [ 225 | getFixturePath('htmlPartial.html'), 226 | getFixturePath('subdir/htmlPartial.html'), 227 | getFixturePath('htmlPartial.html.jade'), 228 | getFixturePath('subdir/htmlPartial.html.jade'), 229 | ]; 230 | assert.deepEqual(dependencies, paths); 231 | done(); 232 | }); 233 | }); 234 | }); 235 | 236 | describe('reverseArgs', function () { 237 | it('should allow path, source args to be switched', function (done) { 238 | var progenyConfig = { 239 | potentialDeps: true, 240 | reverseArgs: true 241 | }; 242 | 243 | progeny(progenyConfig)('@require bar\na=5px\n.test\n\tborder-radius a', 'foo.styl', function (err, deps) { 244 | assert.deepEqual(deps, ['bar.styl', 'bar/index.styl']); 245 | done(); 246 | }); 247 | }); 248 | }); 249 | 250 | describe('regexes', function () { 251 | describe('Stylus', function () { 252 | it('should get Stylus @import statements', function (done) { 253 | var progenyConfig = { potentialDeps: true }; 254 | progeny(progenyConfig)(getFixturePath('imports/foo.styl'), function (err, deps) { 255 | assert.deepEqual(deps, [getFixturePath('imports/bar.styl')]); 256 | done(); 257 | }); 258 | }); 259 | }); 260 | 261 | describe('Protobuf', function () { 262 | it('should get proto import statements', function (done) { 263 | var progenyConfig = { potentialDeps: true }; 264 | progeny(progenyConfig)(getFixturePath('imports/foo.proto'), function (err, deps) { 265 | assert.deepEqual(deps, [ 266 | getFixturePath('imports/bar.proto'), 267 | getFixturePath('imports/baz.proto'), 268 | ]); 269 | done(); 270 | }); 271 | }); 272 | }); 273 | 274 | describe('LESS', function () { 275 | it('should get normal LESS import statements', function (done) { 276 | var progenyConfig = { potentialDeps: true }; 277 | progeny(progenyConfig)(getFixturePath('imports/foo.less'), function (err, deps) { 278 | assert.deepEqual(deps, [getFixturePath('imports/bar.less')]); 279 | done(); 280 | }); 281 | }); 282 | 283 | it('should get LESS import statements with one or more options', function (done) { 284 | var progenyConfig = { potentialDeps: true }; 285 | progeny(progenyConfig)(getFixturePath('imports/foo-options.less'), function (err, deps) { 286 | assert.deepEqual(deps, [getFixturePath('imports/bar.less'), getFixturePath('imports/baz.less')]); 287 | done(); 288 | }); 289 | }); 290 | 291 | it('should get LESS imports with the url() function', function (done) { 292 | var progenyConfig = { potentialDeps: true }; 293 | progeny(progenyConfig)(getFixturePath('imports/foo-url.less'), function (err, deps) { 294 | assert.deepEqual(deps, [ 295 | getFixturePath('imports/bar.less'), 296 | getFixturePath('imports/baz.less'), 297 | getFixturePath('imports/qux.less') 298 | ]); 299 | done(); 300 | }); 301 | }); 302 | }); 303 | }); 304 | 305 | describe('Debug mode', function () { 306 | beforeEach(function () { 307 | sinon.spy(console, 'log'); 308 | }); 309 | 310 | it('should print dependencies list when debug is set to true', function (done) { 311 | progeny({ debug: true, potentialDeps: true })(getFixturePath('imports/foo.less'), function (err, deps) { 312 | assert(console.log.callCount === 2); 313 | done(); 314 | }); 315 | }); 316 | 317 | afterEach(function () { 318 | console.log.restore(); 319 | }); 320 | }); 321 | 322 | describe('skipComments', function () { 323 | var progenyConfig = { 324 | potentialDeps: true, 325 | skipComments: true 326 | }; 327 | 328 | it('should strip line comment without CRLF', function (done) { 329 | var css = '@import "bar"; // @import "baz";'; 330 | progeny(progenyConfig)('main.less', css, function (err, deps) { 331 | assert.deepEqual(deps, ['bar.less']); 332 | done(); 333 | }); 334 | }); 335 | 336 | it('should strip line comment with CRLF', function (done) { 337 | var css = ' // @import "baz"; \r\n @import "bar";'; 338 | progeny(progenyConfig)('main.less', css, function (err, deps) { 339 | assert.deepEqual(deps, ['bar.less']); 340 | done(); 341 | }); 342 | }); 343 | 344 | it('should strip line comment with CR', function (done) { 345 | var css = ' // @import "baz"; \r @import "bar";'; 346 | progeny(progenyConfig)('main.less', css, function (err, deps) { 347 | assert.deepEqual(deps, ['bar.less']); 348 | done(); 349 | }); 350 | }); 351 | 352 | it('should strip line comment with LF', function (done) { 353 | var css = ' // @import "baz"; \n @import "bar";'; 354 | progeny(progenyConfig)('main.less', css, function (err, deps) { 355 | assert.deepEqual(deps, ['bar.less']); 356 | done(); 357 | }); 358 | }); 359 | 360 | it('should strip block comment', function (done) { 361 | var css = ' @import "t1"; /* @import "baz"; */ \n @import "t2";'; 362 | progeny(progenyConfig)('main.less', css, function (err, deps) { 363 | assert.deepEqual(deps, ['t1.less', 't2.less']); 364 | done(); 365 | }); 366 | }); 367 | 368 | it('should strip block multiline comment', function (done) { 369 | var css = ' @import "t1"; /* @import "baz"; \n @import "bar"; */ \n @import "t2";'; 370 | progeny(progenyConfig)('main.less', css, function (err, deps) { 371 | assert.deepEqual(deps, ['t1.less', 't2.less']); 372 | done(); 373 | }); 374 | }); 375 | 376 | it('should be not greedy with multiline comments', function (done) { 377 | var css = ' @import "t1"; /* @import "baz"; \n @import "bar"; */ \n @import "t2"; */'; 378 | progeny(progenyConfig)('main.less', css, function (err, deps) { 379 | assert.deepEqual(deps, ['t1.less', 't2.less']); 380 | done(); 381 | }); 382 | }); 383 | }); 384 | 385 | describe('resolver', function () { 386 | it('should use custom import resolution', function (done) { 387 | var css = '@import "~bar";'; 388 | progeny({ 389 | potentialDeps: true, 390 | resolver: function (depFilename, parentDir, parentFilename) { 391 | if (depFilename.startsWith('~')) { 392 | var absPath = path.resolve(path.join('.', 'node_modules', depFilename.substr(1))); 393 | return path.relative(parentDir, absPath); 394 | } 395 | }, 396 | })('main.less', css, function (err, deps) { 397 | assert.deepEqual(deps, ['node_modules/bar.less']); 398 | done(); 399 | }); 400 | }); 401 | 402 | it('should skip import', function (done) { 403 | var css = '@import "~bar";'; 404 | progeny({ 405 | potentialDeps: true, 406 | resolver: function (depFilename, parentDir, parentFilename) { 407 | if (depFilename.startsWith('~')) { 408 | return false; 409 | } 410 | }, 411 | })('main.less', css, function (err, deps) { 412 | assert.deepEqual(deps, []); 413 | done(); 414 | }); 415 | }); 416 | 417 | it('should not skip import if resolved as true', function (done) { 418 | var css = '@import "bar";'; 419 | progeny({ 420 | potentialDeps: true, 421 | resolver: function (depFilename, parentDir, parentFilename) { 422 | return true; 423 | }, 424 | })('main.less', css, function (err, deps) { 425 | assert.deepEqual(deps, ['bar.less']); 426 | done(); 427 | }); 428 | }); 429 | 430 | it('should not skip import if resolved as undefined', function (done) { 431 | var css = '@import "bar";'; 432 | progeny({ 433 | potentialDeps: true, 434 | resolver: function (depFilename, parentDir, parentFilename) { 435 | return; 436 | }, 437 | })('main.less', css, function (err, deps) { 438 | assert.deepEqual(deps, ['bar.less']); 439 | done(); 440 | }); 441 | }); 442 | 443 | it('should not skip import if resolved as null', function (done) { 444 | var css = '@import "bar";'; 445 | progeny({ 446 | potentialDeps: true, 447 | resolver: function (depFilename, parentDir, parentFilename) { 448 | return null; 449 | }, 450 | })('main.less', css, function (err, deps) { 451 | assert.deepEqual(deps, ['bar.less']); 452 | done(); 453 | }); 454 | }); 455 | }); 456 | 457 | describe('multipass', function () { 458 | var regexps = [ 459 | /(?:'[^']+\.twig')|(?:"[^"]+\.twig")/g, 460 | /^.(.+).$/ 461 | ]; 462 | 463 | it('should resolve multiple dependencies in one line', function (done) { 464 | progeny({ 465 | potentialDeps: true, 466 | multipass: regexps 467 | })('base.twig', '{% include ["partial1.twig", "partial2.twig"] %}', function (err, deps) { 468 | assert.deepEqual(deps, ['partial1.twig', 'partial2.twig']); 469 | done(); 470 | }); 471 | }); 472 | 473 | it('should return empty array when there are no dependencies', function (done) { 474 | progeny({ 475 | potentialDeps: true, 476 | multipass: regexps 477 | })('base.twig', '{# No dependency here! #}', function (err, deps) { 478 | assert.deepEqual(deps, []); 479 | done(); 480 | }); 481 | }); 482 | }); 483 | }); 484 | --------------------------------------------------------------------------------