├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── index.js └── sort.js ├── package.json └── test ├── fixture ├── definesA.js ├── definesB.js ├── definesNg.js ├── definesNgLocale.js ├── dependsOnA.js ├── unrelated1.js ├── unrelated2.js ├── unrelated3.js └── unrelated4.js └── spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /atlassian-ide-plugin.xml 3 | node_modules 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "bitwise": false, 4 | "camelcase": true, 5 | "eqeqeq": true, 6 | "forin": true, 7 | "freeze": true, 8 | "immed": true, 9 | "indent": 4, 10 | "latedef": "nofunc", 11 | "newcap": true, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "undef": true, 15 | "unused": true, 16 | "strict": true, 17 | "trailing": true, 18 | "smarttabs": true, 19 | "predef": [ 20 | "describe", 21 | "it", 22 | "beforeEach", 23 | "afterEach" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.10" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 wilsonjackson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | karma-angular-filesort [![Build Status](https://travis-ci.org/wilsonjackson/karma-angular-filesort.svg?branch=master)](https://travis-ci.org/wilsonjackson/karma-angular-filesort) 2 | ====================== 3 | 4 | > Sorts your AngularJS files to avoid `[$injector:nomod]` errors. 5 | 6 | This plugin owes its existence to [gulp-angular-filesort](https://github.com/klei/gulp-angular-filesort), on which it is heavily based. 7 | 8 | Installation 9 | ------------ 10 | 11 | npm install --save-dev karma-angular-filesort 12 | 13 | Compatibility 14 | ------------- 15 | 16 | For Karma version 0.13.x, use version ~1.0 of this plugin. For older Karma versions, use 0.1. 17 | 18 | Configuration 19 | ------------- 20 | 21 | A simple example configuration would look something like this: 22 | 23 | ```js 24 | // karma.conf.js 25 | module.exports = function(config) { 26 | config.set({ 27 | frameworks: ['jasmine', 'angular-filesort'], 28 | 29 | files: [ 30 | 'bower_components/angular/angular.js', 31 | 'bower_components/angular-mocks/angular-mocks.js', 32 | 'app/**/*.js', 33 | 'test/**/*.js' 34 | ], 35 | 36 | angularFilesort: { 37 | whitelist: [ 38 | 'app/**/*.js' 39 | ] 40 | } 41 | }); 42 | }; 43 | ``` 44 | 45 | ### Whitelist? 46 | 47 | `karma-angular-filesort` will sort the narrowest possible subset of your files by selecting only files that reference angular modules and sorting them in-place. This alone can't prevent all issues though, as certain other files you're obliged to tell Karma about may also define modules — such as Angular itself — and sorting such files can be problematic. 48 | 49 | The `whitelist` config option allows you to further narrow the subset of files `karma-angular-filesort` will sort for you. Each path in the whitelist array will be resolved against Karma's `basePath`. Patterns are supported via [minimatch](https://github.com/isaacs/minimatch). 50 | 51 | License 52 | ------- 53 | 54 | MIT 55 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sort = require('./sort.js'); 4 | var path = require('path'); 5 | 6 | var angularFilesort = function(emitter, logger, basePath, config) { 7 | config = typeof config === 'object' ? config : {}; 8 | var log = logger.create('karma-angular-filesort'); 9 | 10 | // Normalize whitelist paths against basePath 11 | config.whitelist = (config.whitelist || []).map(function (subPath) { 12 | return path.resolve(basePath, subPath); 13 | }); 14 | 15 | // The file list is sorted by intercepting the file_list_modified event as Vojta Jina describes here: 16 | // https://github.com/karma-runner/karma/issues/851#issuecomment-30290071 17 | var originalEmit = emitter.emit; 18 | emitter.emit = function (event, files) { 19 | if (event === 'file_list_modified') { 20 | // Only included files are sorted, as they're the ones loaded into the document 21 | files.included = sort(files.included, log, config); 22 | originalEmit.call(emitter, event, files); 23 | } else { 24 | originalEmit.apply(emitter, arguments); 25 | } 26 | }; 27 | }; 28 | 29 | angularFilesort.$inject = ['emitter', 'logger', 'config.basePath', 'config.angularFilesort']; 30 | 31 | module.exports = { 32 | 'framework:angular-filesort': ['factory', angularFilesort] 33 | }; 34 | -------------------------------------------------------------------------------- /lib/sort.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This file is a modified version of angular-filesort, adapted to operate on Karma's file object. 4 | // Original version is here: 5 | // https://github.com/klei/gulp-angular-filesort/blob/b3ae4a01bf72a3b2434239dc1c84cab156c8e73f/index.js 6 | 7 | var ngDep = require('ng-dependencies'); 8 | var toposort = require('toposort'); 9 | var os = require('os'); 10 | var minimatch = require('minimatch'); 11 | 12 | var ANGULAR_MODULE = 'ng'; 13 | 14 | module.exports = function (files, log, config) { 15 | var moduleFiles = {}; 16 | var sortNodes = []; 17 | var sortEdges = []; 18 | var subsetStart = null; 19 | 20 | // Examine the contents of each file and categorize it as either to-be-sorted or not-to-be-sorted: 21 | files = files.filter(function (file, index) { 22 | var willBeSorted = false; 23 | var deps; 24 | 25 | if (config.whitelist.length && !inWhitelist(file.path, config.whitelist)) { 26 | return true; 27 | } 28 | 29 | try { 30 | deps = ngDep(file.content); 31 | } catch (err) { 32 | log.debug('Error in parsing: "' + file.path + '", ' + err.message); 33 | return true; 34 | } 35 | 36 | if (deps.modules) { 37 | // Store references to each file with a declaration: 38 | Object.keys(deps.modules).forEach(function (name) { 39 | moduleFiles[name] = file; 40 | if (name !== ANGULAR_MODULE && name !== 'ngLocale') { 41 | willBeSorted = true; 42 | } 43 | }); 44 | } 45 | 46 | if (deps.dependencies) { 47 | // Add each file with dependencies to the array to sort: 48 | deps.dependencies.forEach(function (dep) { 49 | if (isDependecyUsedInAnyDeclaration(dep, deps)) { 50 | return; 51 | } 52 | if (dep === ANGULAR_MODULE) { 53 | return; 54 | } 55 | sortEdges.push([file, dep]); 56 | willBeSorted = true; 57 | }); 58 | } 59 | 60 | if (willBeSorted) { 61 | // Store the position of the first file to be sorted, as that's where the sorted subset will be re-inserted: 62 | if (subsetStart === null) { 63 | subsetStart = index; 64 | } 65 | sortNodes.push(file); 66 | } 67 | return !willBeSorted; 68 | }); 69 | 70 | if (sortNodes.length === 0) { 71 | // No angular module references found, so return original array: 72 | return files; 73 | } 74 | 75 | // Convert all module names to actual files with declarations: 76 | for (var i = 0; i < sortEdges.length; i++) { 77 | var moduleName = sortEdges[i][1]; 78 | var declarationFile = moduleFiles[moduleName]; 79 | if (declarationFile) { 80 | sortEdges[i][1] = declarationFile; 81 | } else { 82 | // Depending on module outside list (possibly a 3rd party one), 83 | // don't care when sorting: 84 | sortEdges.splice(i--, 1); 85 | } 86 | } 87 | 88 | // Sort `files` with `toSort` as dependency tree: 89 | Array.prototype.splice.apply(files, [subsetStart, 0].concat(toposort.array(sortNodes, sortEdges).reverse())); 90 | 91 | log.debug('Sorted files:' + os.EOL + files.map(function (file) { 92 | return '\t' + file.path; 93 | }).join(os.EOL)); 94 | 95 | return files; 96 | }; 97 | 98 | function inWhitelist(filePath, whitelist) { 99 | return whitelist.some(function (whitelistPath) { 100 | return minimatch(filePath, whitelistPath); 101 | }); 102 | } 103 | 104 | function isDependecyUsedInAnyDeclaration (dependency, ngDeps) { 105 | if (!ngDeps.modules) { 106 | return false; 107 | } 108 | if (dependency in ngDeps.modules) { 109 | return true; 110 | } 111 | return Object.keys(ngDeps.modules).some(function (module) { 112 | return ngDeps.modules[module].indexOf(dependency) > -1; 113 | }); 114 | } 115 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "karma-angular-filesort", 3 | "version": "1.0.2", 4 | "description": "Sort AngularJS files before a Karma run.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/wilsonjackson/karma-angular-filesort.git" 12 | }, 13 | "keywords": [ 14 | "karma-plugin", 15 | "karma-framework", 16 | "karma-adapter" 17 | ], 18 | "author": "Majid Burney ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/wilsonjackson/karma-angular-filesort/issues" 22 | }, 23 | "dependencies": { 24 | "minimatch": "^3.0.3", 25 | "ng-dependencies": "^0.3.0", 26 | "q": "^1.0.1", 27 | "toposort": "^0.2.10" 28 | }, 29 | "devDependencies": { 30 | "chai": "^1.9.2", 31 | "mocha": "^3.1.0", 32 | "sinon": "^1.10.3", 33 | "sinon-chai": "^2.6.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/fixture/definesA.js: -------------------------------------------------------------------------------- 1 | angular.module('A', []); 2 | -------------------------------------------------------------------------------- /test/fixture/definesB.js: -------------------------------------------------------------------------------- 1 | angular.module('B', []); 2 | -------------------------------------------------------------------------------- /test/fixture/definesNg.js: -------------------------------------------------------------------------------- 1 | angular.module('ng', ['ngLocale']); 2 | -------------------------------------------------------------------------------- /test/fixture/definesNgLocale.js: -------------------------------------------------------------------------------- 1 | angular.module('ngLocale', []); 2 | -------------------------------------------------------------------------------- /test/fixture/dependsOnA.js: -------------------------------------------------------------------------------- 1 | angular.module('A').controller('Ctrl', function () {}); 2 | -------------------------------------------------------------------------------- /test/fixture/unrelated1.js: -------------------------------------------------------------------------------- 1 | function hasNothingToDoWithAngular() { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture/unrelated2.js: -------------------------------------------------------------------------------- 1 | var hasNothingToDoWithAngular = true; 2 | -------------------------------------------------------------------------------- /test/fixture/unrelated3.js: -------------------------------------------------------------------------------- 1 | hasNothingToDoWithAngular(); 2 | -------------------------------------------------------------------------------- /test/fixture/unrelated4.js: -------------------------------------------------------------------------------- 1 | angular.copy({usesAngular: 'butDoesntReferenceModules'}); 2 | -------------------------------------------------------------------------------- /test/spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var expect = chai.expect; 5 | var sinon = require('sinon'); 6 | chai.use(require('sinon-chai')); 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | var sortFiles = require('../lib/index.js')['framework:angular-filesort'][1]; 10 | var Q = require('q'); 11 | 12 | describe('karma-angular-filesort', function () { 13 | var emit; 14 | var emitter; 15 | var logger; 16 | 17 | beforeEach(function () { 18 | emit = sinon.spy(); 19 | emitter = { 20 | emit: emit 21 | }; 22 | var noop = function () {}; 23 | logger = { 24 | create: function () { 25 | return {error: noop, warn: noop, info: noop, debug: noop}; 26 | } 27 | }; 28 | }); 29 | 30 | function loadFixture(file) { 31 | var filePath = path.join(__dirname, 'fixture', file); 32 | return { 33 | path: filePath, 34 | content: fs.readFileSync(filePath).toString() 35 | }; 36 | } 37 | 38 | function emitFiles(files) { 39 | emitter.emit('file_list_modified', files.reduce(function (batch, file) { 40 | var fixture = loadFixture(file); 41 | batch.served.push(fixture); 42 | batch.included.push(fixture); 43 | return batch; 44 | }, {served: [], included: []})); 45 | } 46 | 47 | function verifyPromise(promise, verify, done) { 48 | Q.resolve(promise) 49 | .then(verify) 50 | .then(done) 51 | .catch(function (error) { 52 | done(error); 53 | }); 54 | } 55 | 56 | it('should call the original emit method with emitter scope', function () { 57 | sortFiles(emitter, logger, __dirname); 58 | emitFiles(['unrelated1.js']); 59 | expect(emit).to.have.been.calledOn(emitter); 60 | }); 61 | 62 | it('should reorder files with module definition dependencies', function (done) { 63 | sortFiles(emitter, logger, __dirname); 64 | emitFiles(['dependsOnA.js', 'definesA.js']); 65 | expect(emit).to.have.been.calledWith('file_list_modified', sinon.match.any); 66 | verifyPromise(emit.args[0][1], function (files) { 67 | expect(files.included[0].path).to.match(/definesA\.js$/); 68 | expect(files.included[1].path).to.match(/dependsOnA\.js$/); 69 | }, done); 70 | }); 71 | 72 | it('should preserve the order of unaffected files', function (done) { 73 | sortFiles(emitter, logger, __dirname); 74 | emitFiles(['unrelated1.js', 'unrelated2.js', 'dependsOnA.js', 'definesA.js', 'unrelated3.js', 'unrelated4.js']); 75 | expect(emit).to.have.been.calledWith('file_list_modified', sinon.match.any); 76 | verifyPromise(emit.args[0][1], function (files) { 77 | expect(files.included[0].path).to.match(/unrelated1\.js$/); 78 | expect(files.included[1].path).to.match(/unrelated2\.js$/); 79 | expect(files.included[2].path).to.match(/definesA\.js$/); 80 | expect(files.included[3].path).to.match(/dependsOnA\.js$/); 81 | expect(files.included[4].path).to.match(/unrelated3\.js$/); 82 | expect(files.included[5].path).to.match(/unrelated4\.js$/); 83 | }, done); 84 | }); 85 | 86 | it('should put sorted files at the beginning of the subset they define', function (done) { 87 | sortFiles(emitter, logger, __dirname); 88 | emitFiles(['unrelated1.js', 'dependsOnA.js', 'unrelated2.js', 'definesA.js', 'unrelated3.js']); 89 | expect(emit).to.have.been.calledWith('file_list_modified', sinon.match.any); 90 | verifyPromise(emit.args[0][1], function (files) { 91 | expect(files.included[0].path).to.match(/unrelated1\.js/); 92 | expect(files.included[1].path).to.match(/definesA\.js/); 93 | expect(files.included[2].path).to.match(/dependsOnA\.js/); 94 | expect(files.included[3].path).to.match(/unrelated2\.js/); 95 | expect(files.included[4].path).to.match(/unrelated3\.js/); 96 | }, done); 97 | }); 98 | 99 | it('should not reorder ng and ngLocale (Angular own) modules', function (done) { 100 | sortFiles(emitter, logger, __dirname); 101 | emitFiles(['unrelated1.js', 'unrelated2.js', 'dependsOnA.js', 'definesA.js', 'definesNg.js', 'unrelated3.js', 'unrelated4.js', 'definesNgLocale.js']); 102 | expect(emit).to.have.been.calledWith('file_list_modified', sinon.match.any); 103 | verifyPromise(emit.args[0][1], function (files) { 104 | expect(files.included[0].path).to.match(/unrelated1\.js$/); 105 | expect(files.included[1].path).to.match(/unrelated2\.js$/); 106 | expect(files.included[2].path).to.match(/definesA\.js$/); 107 | expect(files.included[3].path).to.match(/dependsOnA\.js$/); 108 | expect(files.included[4].path).to.match(/definesNg\.js$/); 109 | expect(files.included[5].path).to.match(/unrelated3\.js$/); 110 | expect(files.included[6].path).to.match(/unrelated4\.js$/); 111 | expect(files.included[7].path).to.match(/definesNgLocale\.js$/); 112 | }, done); 113 | }); 114 | 115 | it('should allow a whitelist to restrict which files are reordered', function (done) { 116 | sortFiles(emitter, logger, __dirname, {whitelist: ['fixture/definesA.js', 'fixture/dependsOnA.js']}); 117 | emitFiles(['definesB.js', 'unrelated1.js', 'dependsOnA.js', 'definesA.js']); 118 | expect(emit).to.have.been.calledWith('file_list_modified', sinon.match.any); 119 | verifyPromise(emit.args[0][1], function (files) { 120 | expect(files.included[0].path).to.match(/definesB\.js$/); 121 | expect(files.included[1].path).to.match(/unrelated1\.js$/); 122 | expect(files.included[2].path).to.match(/definesA\.js$/); 123 | expect(files.included[3].path).to.match(/dependsOnA\.js$/); 124 | }, done); 125 | }); 126 | 127 | it('should support pattern matching in the whitelist', function (done) { 128 | sortFiles(emitter, logger, __dirname, {whitelist: ['fixture/*A.js']}); 129 | emitFiles(['definesB.js', 'unrelated1.js', 'dependsOnA.js', 'definesA.js']); 130 | expect(emit).to.have.been.calledWith('file_list_modified', sinon.match.any); 131 | verifyPromise(emit.args[0][1], function (files) { 132 | expect(files.included[0].path).to.match(/definesB\.js$/); 133 | expect(files.included[1].path).to.match(/unrelated1\.js$/); 134 | expect(files.included[2].path).to.match(/definesA\.js$/); 135 | expect(files.included[3].path).to.match(/dependsOnA\.js$/); 136 | }, done); 137 | }); 138 | 139 | it('should not reorder files if no angular modules are found', function (done) { 140 | sortFiles(emitter, logger, __dirname); 141 | emitFiles(['unrelated2.js', 'unrelated1.js']); 142 | expect(emit).to.have.been.calledWith('file_list_modified', sinon.match.any); 143 | verifyPromise(emit.args[0][1], function (files) { 144 | expect(files.included[0].path).to.match(/unrelated2\.js$/); 145 | expect(files.included[1].path).to.match(/unrelated1\.js$/); 146 | }, done); 147 | }); 148 | 149 | it('should pass through other events', function () { 150 | sortFiles(emitter, logger, __dirname); 151 | emitter.emit('some_other_event', 'arg1', 'arg2', 'arg3'); 152 | expect(emit).to.have.been.calledWith('some_other_event', 'arg1', 'arg2', 'arg3'); 153 | }); 154 | }); 155 | --------------------------------------------------------------------------------