├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── LICENSE-MIT ├── README.md ├── changelog.md ├── gruntfile.js ├── lib └── util.js ├── package.json ├── tasks └── newer.js └── test ├── .jshintrc ├── helper.js ├── integration ├── fixtures │ ├── newer-clean-dest │ │ ├── gruntfile.js │ │ └── src │ │ │ ├── one.coffee │ │ │ └── two.coffee │ ├── newer-dest │ │ ├── gruntfile.js │ │ └── src │ │ │ ├── one.coffee │ │ │ └── two.coffee │ ├── newer-modify-none │ │ ├── gruntfile.js │ │ └── src │ │ │ ├── one.js │ │ │ └── two.js │ ├── newer-modify-one │ │ ├── gruntfile.js │ │ └── src │ │ │ ├── one.js │ │ │ └── two.js │ ├── newer-override │ │ ├── gruntfile.js │ │ └── src │ │ │ ├── one.js │ │ │ ├── three.js │ │ │ └── two.js │ ├── newer-reconfigure │ │ ├── gruntfile.js │ │ └── src │ │ │ ├── one.coffee │ │ │ └── two.coffee │ └── newer-tolerance │ │ ├── gruntfile.js │ │ └── src │ │ ├── one.coffee │ │ └── two.coffee ├── newer-clean-dest.spec.js ├── newer-dest.spec.js ├── newer-modify-none.spec.js ├── newer-modify-one.spec.js ├── newer-override.spec.js ├── newer-reconfigure.spec.js ├── newer-tolerance.spec.js └── tasks │ └── index.js └── lib └── util.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | npm-debug.log 3 | /.cache/ 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "indent": 2, 5 | "latedef": true, 6 | "newcap": true, 7 | "nonew": true, 8 | "quotmark": "single", 9 | "undef": true, 10 | "trailing": true, 11 | "maxlen": 80, 12 | "globals": { 13 | "exports": true, 14 | "module": false, 15 | "process": false, 16 | "require": false, 17 | "__dirname": false 18 | } 19 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.8" 5 | 6 | before_install: 7 | - '[ "${TRAVIS_NODE_VERSION}" != "0.8" ] || npm install -g npm@1.4.28' 8 | - npm install -g npm@latest 9 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Tim Schaub 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grunt-newer 2 | 3 | Configure [Grunt](http://gruntjs.com/) tasks to run with newer files only. 4 | 5 | **Synopsis:** The [`newer`](#newer) task will configure another task to run with `src` files that are *a)* newer than the `dest` files or *b)* newer than the last successful run (if there are no `dest` files). See below for examples and more detail. 6 | 7 | ## Getting Started 8 | This plugin requires Grunt `~0.4.1` 9 | 10 | If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [`gruntfile.js`](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, you may install this plugin with this command: 11 | 12 | ```shell 13 | npm install grunt-newer --save-dev 14 | ``` 15 | 16 | Once the plugin has been installed, it may be enabled inside your `gruntfile.js` with this line: 17 | 18 | ```js 19 | grunt.loadNpmTasks('grunt-newer'); 20 | ``` 21 | 22 | 23 | ## The `newer` task 24 | 25 | The `newer` task doesn't require any special configuration. To use it, just add `newer` as the first argument when running other tasks. 26 | 27 | For example, if you want to use [Uglify](https://npmjs.org/package/grunt-contrib-uglify) to minify your source files only when one or more of them is newer than the previously minified destination file, configure the `uglify` task as you would otherwise, and then register a task with `newer` at the front. 28 | 29 | ```js 30 | grunt.initConfig({ 31 | uglify: { 32 | all: { 33 | files: { 34 | 'dest/app.min.js': ['src/**/*.js'] 35 | } 36 | } 37 | } 38 | }); 39 | 40 | grunt.loadNpmTasks('grunt-contrib-uglify'); 41 | grunt.loadNpmTasks('grunt-newer'); 42 | 43 | grunt.registerTask('minify', ['newer:uglify:all']); 44 | ``` 45 | 46 | With the above configuration the `minify` task will only run `uglify` if one or more of the `src/**/*.js` files is newer than the `dest/app.min.js` file. 47 | 48 | The above example shows how the `newer` task works with other tasks that specify both `src` and `dest` files. In this case, the modification time of `src` files are compared to modification times of corresponding `dest` files to determine which `src` files to include. 49 | 50 | The `newer` task can also be used with tasks that don't generate any `dest` files. In this case, `newer` will only use files that are newer than the last successful run of the same task. 51 | 52 | For example, if you want to run [JSHint](https://npmjs.org/package/grunt-contrib-jshint) on only those files that have been modified since the last successful run, configure the `jshint` task as you would otherwise, and then register a task with `newer` at the front. 53 | 54 | ```js 55 | grunt.initConfig({ 56 | jshint: { 57 | all: { 58 | src: 'src/**/*.js' 59 | } 60 | } 61 | }); 62 | 63 | grunt.loadNpmTasks('grunt-contrib-jshint'); 64 | grunt.loadNpmTasks('grunt-newer'); 65 | 66 | grunt.registerTask('lint', ['newer:jshint:all']); 67 | ``` 68 | 69 | With the above configuration, running `grunt lint` will configure your `jshint:all` task to use only files in the `jshint.all.src` config that have been modified since the last successful run of the same task. The first time the `jshint:newer:all` task runs, all source files will be used. After that, only the files you modify will be run through the linter. 70 | 71 | Another example is to use the `newer` task in conjunction with `watch`. For example, you might want to set up a watch to run a linter on all your `.js` files whenever one changes. With the `newer` task, instead of re-running the linter on all files, you only need to run it on the files that changed. 72 | 73 | ```js 74 | var srcFiles = 'src/**/*.js'; 75 | 76 | grunt.initConfig({ 77 | jshint: { 78 | all: { 79 | src: srcFiles 80 | } 81 | }, 82 | watch: { 83 | all: { 84 | files: srcFiles, 85 | tasks: ['newer:jshint:all'] 86 | } 87 | } 88 | }); 89 | 90 | grunt.loadNpmTasks('grunt-contrib-jshint'); 91 | grunt.loadNpmTasks('grunt-contrib-watch'); 92 | grunt.loadNpmTasks('grunt-newer'); 93 | 94 | ``` 95 | 96 | With the above configuration, running `grunt jshint watch` will first lint all your files with `jshint` and then set up a watch. Whenever one of your source files changes, the `jshint` task will be run on just the modified file. 97 | 98 | *Note:* If your task is configured with `dest` files, `newer` will run your task with only those files that are newer than the corresponding `dest` files. 99 | 100 | ## Options for the `newer` task 101 | 102 | In most cases, you shouldn't need to add any special configuration for the `newer` task. Just `grunt.loadNpmTasks('grunt-newer')` and you can use `newer` as a prefix to your other tasks. The options below are available for advanced usage. 103 | 104 | #### options.cache 105 | * type: `string` 106 | * default: `node_modules/grunt-newer/.cache` 107 | 108 | To keep track of timestamps for successful runs, the `newer` task writes to a cache directory. The default is to use a `.cache` directory within the `grunt-newer` installation directory. If you need timestamp info to be written to a different location, configure the task with a `cache` option. 109 | 110 | Example use of the `cache` option: 111 | 112 | ```js 113 | grunt.initConfig({ 114 | newer: { 115 | options: { 116 | cache: 'path/to/custom/cache/directory' 117 | } 118 | } 119 | }); 120 | ``` 121 | 122 | #### options.override 123 | * type: `function(Object, function(boolean))` 124 | * default: `null` 125 | 126 | The `newer` task determines which files to include for a specific task based on file modification time. There are occassions where you may want to include a file even if it has not been modified. For example, if a LESS file imports some other files, you will want to include it if any of the imports have been modified. To support this, you can provide an `override` function that takes two arguments: 127 | 128 | * **details** - `Object` 129 | * **task** - `string` The currently running task name. 130 | * **target** - `string` The currently running target name. 131 | * **path** - `string` The path to a `src` file that appears to be "older" (not modified since the time below). 132 | * **time** - `Date` The comparison time. For tasks with `dest` files, this is the modification time of the `dest` file. For tasks without `dest` files, this is the last successful run time of the same task. 133 | * **include** - `function(boolean)` A callback that determines whether this `src` file should be included. Call with `true` to include or `false` to exclude the file. 134 | 135 | Example use of the `override` option: 136 | 137 | ```js 138 | grunt.initConfig({ 139 | newer: { 140 | options: { 141 | override: function(detail, include) { 142 | if (detail.task === 'less') { 143 | checkForModifiedImports(detail.path, detail.time, include); 144 | } else { 145 | include(false); 146 | } 147 | } 148 | } 149 | } 150 | }); 151 | ``` 152 | 153 | #### options.tolerance 154 | * type: `number` (milliseconds) 155 | * default: `0` 156 | 157 | The `newer` tasks compares the file modification times of the source and destination files with millisecond precision. 158 | On some file systems this causes destination files to always be considered older because of imprecision in the registration of modification times. 159 | 160 | If your tasks are always run even though the source files are not modified use the `tolerance` option to compensate for this imprecision. 161 | 162 | In most cases setting the option to `1000` milliseconds should be enough. If the file system is very imprecise use a higher value. 163 | 164 | Example use of the `tolerance` option: 165 | 166 | ```js 167 | grunt.initConfig({ 168 | newer: { 169 | options: { 170 | tolerance: 1000 171 | } 172 | } 173 | }); 174 | ``` 175 | 176 | ## That's it 177 | 178 | Please [submit an issue](https://github.com/tschaub/grunt-newer/issues) if you encounter any trouble. Contributions or suggestions for improvements welcome! 179 | 180 | [![Current Status](https://secure.travis-ci.org/tschaub/grunt-newer.png?branch=master)](https://travis-ci.org/tschaub/grunt-newer) 181 | 182 | ## Known limitations 183 | 184 | The `newer` task relies on Grunt's convention for specifying [`src`/`dest` mappings](http://gruntjs.com/configuring-tasks#files). So it should be expected to work with two types of tasks: 185 | 186 | 1) Tasks that specify both `src` and `dest` files. In this case, the task prefixed by `newer` will be configured to run with `src` files that are newer than the corresponding `dest` file (based on the `mtime` of files). 187 | 188 | 2) Tasks that specify only `src` files. In this case, the task prefixed by `newer` will be configured to run with `src` files that are newer than the previous successful run of the same task. 189 | 190 | The `newer` task will *not* work as a prefix for the following tasks: 191 | 192 | * [`grunt-rsync`](http://npmjs.org/package/grunt-rsync) - Though this task specifies `src` and `dest` files, the `dest` file is not generated based on `src` files (instead it is a directory). 193 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.3.0 4 | 5 | * Updated dev dependencies 6 | 7 | ## 1.2.0 8 | 9 | * Add `tolerance` option to account for filesystem time precision (thanks @jorrit, see [#94][#94]) 10 | * Updated dependencies (thanks @jorrit, see [#93][#93]) 11 | 12 | ## 1.1.2 13 | 14 | * Update peer dependency for Grunt (thanks @steveoh, see [#91][91]) 15 | 16 | ## 1.1.1 17 | 18 | * Update license identifier (MIT) 19 | 20 | ## 1.1.0 21 | 22 | * Write current time to timestamp file (thanks @malys, see [#69][69]) 23 | 24 | ## 1.0.0 25 | 26 | * Document that grunt-newer works with grunt-spritesmith >= 3.1.0 (thanks @danez, see [#66][66]) 27 | * Support for an empty list of source files (thanks @ruslansagitov, see [#62][62]) 28 | 29 | ## 0.8.0 30 | 31 | * Support for a single source file that matches the dest file (thanks @btholt, see [#42][42] and [#62][62]) 32 | * Avoid unhandled error when task is aliased (see [#61][61]) 33 | 34 | ## 0.7.0 35 | 36 | * Support for `override` option. In cases where a `src` file should be included even if it has not been modified (e.g. a LESS file whose imports have been modified), the `override` option can be used (see [#35][35]) 37 | 38 | ## 0.6.1 39 | 40 | * When `src` and `dest` files are the same, the previous run time is considered (see [#24][24]) 41 | 42 | ## 0.6.0 43 | 44 | * Deprecated `any-newer` task (`newer` task now handles this automatically, see [#17][17]) 45 | * Deprecated `timestamps` option (use `cache` instead) 46 | * Consolidated `newer-reconfigure` and `newer-timestamp` into single `newer-postrun` task 47 | * Refactor task for easier unit testing (see [#16][16]) 48 | 49 | ## 0.5.4 50 | 51 | * Correctly handle cases where `dest` file is not present (thanks @royriojas, see [#11][11]) 52 | 53 | ## 0.5.3 54 | 55 | * Add `newer-reconfigure` to properly reset task configuration (see [#8][8]) 56 | 57 | ## 0.5.2 58 | 59 | * Fix use of `any-newer` on task with multiple targets (thanks @royriojas, see [#7][7]) 60 | 61 | ## 0.5.1 62 | 63 | * Filter out file objects with no remaining `src` files (see [#6][6]) 64 | 65 | ## 0.5.0 66 | 67 | * Compare `src` file modification times to `dest` files if present (see [#2][2]) 68 | 69 | [2]: https://github.com/tschaub/grunt-newer/pull/2 70 | [6]: https://github.com/tschaub/grunt-newer/pull/6 71 | [7]: https://github.com/tschaub/grunt-newer/pull/7 72 | [8]: https://github.com/tschaub/grunt-newer/pull/8 73 | [11]: https://github.com/tschaub/grunt-newer/pull/11 74 | [16]: https://github.com/tschaub/grunt-newer/pull/16 75 | [17]: https://github.com/tschaub/grunt-newer/pull/17 76 | [24]: https://github.com/tschaub/grunt-newer/pull/24 77 | [35]: https://github.com/tschaub/grunt-newer/pull/35 78 | [42]: https://github.com/tschaub/grunt-newer/pull/42 79 | [61]: https://github.com/tschaub/grunt-newer/pull/61 80 | [62]: https://github.com/tschaub/grunt-newer/pull/62 81 | [66]: https://github.com/tschaub/grunt-newer/pull/66 82 | [69]: https://github.com/tschaub/grunt-newer/pull/69 83 | [91]: https://github.com/tschaub/grunt-newer/pull/91 84 | [93]: https://github.com/tschaub/grunt-newer/pull/93 85 | [94]: https://github.com/tschaub/grunt-newer/pull/94 86 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var path = require('path'); 3 | var fs = require('fs'); 4 | 5 | 6 | /** 7 | * @param {Object} grunt Grunt. 8 | */ 9 | module.exports = function(grunt) { 10 | 11 | var gruntfileSrc = 'gruntfile.js'; 12 | var tasksSrc = ['tasks/**/*.js', 'lib/**/*.js']; 13 | var testSrc = 'test/**/*.spec.js'; 14 | var fixturesJs = 'test/integration/fixtures/**/*.js'; 15 | var fixturesAll = 'test/integration/fixtures/**/*'; 16 | 17 | grunt.initConfig({ 18 | 19 | cafemocha: { 20 | options: { 21 | reporter: 'spec' 22 | }, 23 | all: { 24 | src: testSrc 25 | } 26 | }, 27 | 28 | jshint: { 29 | options: { 30 | jshintrc: true 31 | }, 32 | gruntfile: { 33 | src: gruntfileSrc 34 | }, 35 | tasks: { 36 | src: tasksSrc 37 | }, 38 | tests: { 39 | src: testSrc 40 | }, 41 | fixturesJs: { 42 | src: fixturesJs 43 | } 44 | }, 45 | 46 | watch: { 47 | tasks: { 48 | files: tasksSrc, 49 | tasks: ['cafemocha'] 50 | }, 51 | tests: { 52 | files: testSrc, 53 | tasks: ['newer:cafemocha'] 54 | }, 55 | fixturesAll: { 56 | files: fixturesAll, 57 | tasks: ['cafemocha'] 58 | }, 59 | allJs: { 60 | files: [gruntfileSrc, tasksSrc, testSrc, fixturesJs], 61 | tasks: ['newer:jshint'] 62 | } 63 | } 64 | 65 | }); 66 | 67 | grunt.loadTasks('tasks'); 68 | grunt.loadNpmTasks('grunt-contrib-jshint'); 69 | grunt.loadNpmTasks('grunt-contrib-watch'); 70 | grunt.loadNpmTasks('grunt-cafe-mocha'); 71 | 72 | grunt.registerTask('test', ['newer:jshint', 'cafemocha']); 73 | 74 | grunt.registerTask('default', 'test'); 75 | 76 | }; 77 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | var async = require('async'); 6 | 7 | /** 8 | * Filter a list of files by mtime. 9 | * @param {Array.} paths List of file paths. 10 | * @param {Date} time The comparison time. 11 | * @param {number} tolerance Maximum time in milliseconds that the destination 12 | * file is allowed to be newer than the source file to compensate for 13 | * imprecisions in modification times in file systems. 14 | * @param {function(string, Date, function(boolean))} override Override. 15 | * @param {function(Err, Array.)} callback Callback called with any 16 | * error and a list of files that have mtimes newer than the provided time. 17 | */ 18 | var filterPathsByTime = exports.filterPathsByTime = function(paths, time, 19 | tolerance, override, callback) { 20 | async.map(paths, fs.stat, function(err, stats) { 21 | if (err) { 22 | return callback(err); 23 | } 24 | 25 | var olderPaths = []; 26 | var newerPaths = paths.filter(function(filePath, index) { 27 | var newer = stats[index].mtime - time > tolerance; 28 | if (!newer) { 29 | olderPaths.push(filePath); 30 | } 31 | return newer; 32 | }); 33 | 34 | async.filter(olderPaths, function(filePath, include) { 35 | override(filePath, time, include); 36 | }, function(overrides) { 37 | callback(null, newerPaths.concat(overrides)); 38 | }); 39 | }); 40 | }; 41 | 42 | 43 | /** 44 | * Determine if any of the given files are newer than the provided time. 45 | * @param {Array.} paths List of file paths. 46 | * @param {Date} time The comparison time. 47 | * @param {number} tolerance Maximum time in milliseconds that the destination 48 | * file is allowed to be newer than the source file to compensate for 49 | * imprecisions in modification times in file systems. 50 | * @param {function(string, Date, function(boolean))} override Override. 51 | * @param {function(Err, boolean)} callback Callback called with any error and 52 | * a boolean indicating whether any one of the supplied files is newer than 53 | * the comparison time. 54 | */ 55 | var anyNewer = exports.anyNewer = function(paths, time, tolerance, override, 56 | callback) { 57 | if (paths.length === 0) { 58 | process.nextTick(function() { 59 | callback(null, false); 60 | }); 61 | return; 62 | } 63 | var complete = 0; 64 | function iterate() { 65 | fs.stat(paths[complete], function(err, stats) { 66 | if (err) { 67 | return callback(err); 68 | } 69 | 70 | var pathTime = stats.mtime.getTime(); 71 | var comparisonTime = time.getTime(); 72 | var difference = pathTime - comparisonTime; 73 | 74 | if (difference > tolerance) { 75 | return callback(null, true); 76 | } else { 77 | override(paths[complete], time, function(include) { 78 | if (include) { 79 | callback(null, true); 80 | } else { 81 | ++complete; 82 | if (complete >= paths.length) { 83 | return callback(null, false); 84 | } 85 | iterate(); 86 | } 87 | }); 88 | } 89 | }); 90 | } 91 | iterate(); 92 | }; 93 | 94 | 95 | /** 96 | * Filter a list of file config objects by time. Source files on the provided 97 | * objects are removed if they have not been modified since the provided time 98 | * or any dest file mtime for a dest file on the same object. 99 | * @param {Array.} files A list of Grunt file config objects. These 100 | * are returned from `grunt.task.normalizeMultiTaskFiles` and have a src 101 | * property (Array.) and an optional dest property (string). 102 | * @param {Date} previous Comparison time. 103 | * @param {number} tolerance Maximum time in milliseconds that the destination 104 | * file is allowed to be newer than the source file to compensate for 105 | * imprecisions in modification times in file systems. 106 | * @param {function(string, Date, function(boolean))} override Override. 107 | * @param {function(Error, Array.)} callback Callback called with 108 | * modified file config objects. Objects with no more src files are 109 | * filtered from the results. 110 | */ 111 | var filterFilesByTime = exports.filterFilesByTime = function(files, previous, 112 | tolerance, override, callback) { 113 | async.map(files, function(obj, done) { 114 | if (obj.dest && !(obj.src.length === 1 && obj.dest === obj.src[0])) { 115 | fs.stat(obj.dest, function(err, stats) { 116 | if (err) { 117 | // dest file not yet created, use all src files 118 | return done(null, obj); 119 | } 120 | return anyNewer( 121 | obj.src, stats.mtime, tolerance, override, function(err, any) { 122 | done(err, any && obj); 123 | }); 124 | }); 125 | } else { 126 | filterPathsByTime( 127 | obj.src, previous, tolerance, override, function(err, src) { 128 | if (err) { 129 | return done(err); 130 | } 131 | done(null, {src: src, dest: obj.dest}); 132 | }); 133 | } 134 | }, function(err, results) { 135 | if (err) { 136 | return callback(err); 137 | } 138 | // get rid of file config objects with no src files 139 | callback(null, results.filter(function(obj) { 140 | return obj && obj.src && obj.src.length > 0; 141 | })); 142 | }); 143 | }; 144 | 145 | 146 | /** 147 | * Get path to cached file hash for a target. 148 | * @param {string} cacheDir Path to cache dir. 149 | * @param {string} taskName Task name. 150 | * @param {string} targetName Target name. 151 | * @param {string} filePath Path to file. 152 | * @return {string} Path to hash. 153 | */ 154 | var getHashPath = exports.getHashPath = function(cacheDir, taskName, targetName, 155 | filePath) { 156 | var hashedName = crypto.createHash('md5').update(filePath).digest('hex'); 157 | return path.join(cacheDir, taskName, targetName, 'hashes', hashedName); 158 | }; 159 | 160 | 161 | /** 162 | * Get an existing hash for a file (if it exists). 163 | * @param {string} filePath Path to file. 164 | * @param {string} cacheDir Cache directory. 165 | * @param {string} taskName Task name. 166 | * @param {string} targetName Target name. 167 | * @param {function(Error, string} callback Callback called with an error and 168 | * file hash (or null if the file doesn't exist). 169 | */ 170 | var getExistingHash = exports.getExistingHash = function(filePath, cacheDir, 171 | taskName, targetName, callback) { 172 | var hashPath = getHashPath(cacheDir, taskName, targetName, filePath); 173 | fs.exists(hashPath, function(exists) { 174 | if (!exists) { 175 | return callback(null, null); 176 | } 177 | fs.readFile(hashPath, callback); 178 | }); 179 | }; 180 | 181 | 182 | /** 183 | * Generate a hash (md5sum) of a file contents. 184 | * @param {string} filePath Path to file. 185 | * @param {function(Error, string)} callback Callback called with any error and 186 | * the hash of the file contents. 187 | */ 188 | var generateFileHash = exports.generateFileHash = function(filePath, callback) { 189 | var md5sum = crypto.createHash('md5'); 190 | var stream = new fs.ReadStream(filePath); 191 | stream.on('data', function(chunk) { 192 | md5sum.update(chunk); 193 | }); 194 | stream.on('error', callback); 195 | stream.on('end', function() { 196 | callback(null, md5sum.digest('hex')); 197 | }); 198 | }; 199 | 200 | 201 | /** 202 | * Filter files based on hashed contents. 203 | * @param {Array.} paths List of paths to files. 204 | * @param {string} cacheDir Cache directory. 205 | * @param {string} taskName Task name. 206 | * @param {string} targetName Target name. 207 | * @param {function(Error, Array.)} callback Callback called with any 208 | * error and a filtered list of files that only includes files with hashes 209 | * that are different than the cached hashes for the same files. 210 | */ 211 | var filterPathsByHash = exports.filterPathsByHash = function(paths, cacheDir, 212 | taskName, targetName, callback) { 213 | async.filter(paths, function(filePath, done) { 214 | async.parallel({ 215 | previous: function(cb) { 216 | getExistingHash(filePath, cacheDir, taskName, targetName, cb); 217 | }, 218 | current: function(cb) { 219 | generateFileHash(filePath, cb); 220 | } 221 | }, function(err, hashes) { 222 | if (err) { 223 | return callback(err); 224 | } 225 | done(String(hashes.previous) !== String(hashes.current)); 226 | }); 227 | }, callback); 228 | }; 229 | 230 | 231 | /** 232 | * Filter a list of file config objects based on comparing hashes of src files. 233 | * @param {Array.} files List of file config objects. 234 | * @param {string} taskName Task name. 235 | * @param {string} targetName Target name. 236 | * @param {function(Error, Array.)} callback Callback called with a 237 | * filtered list of file config objects. Object returned will only include 238 | * src files with hashes that are different than any cached hashes. Config 239 | * objects with no src files will be filtered from the list. 240 | */ 241 | var filterFilesByHash = exports.filterFilesByHash = function(files, taskName, 242 | targetName, callback) { 243 | var modified = false; 244 | async.map(files, function(obj, done) { 245 | 246 | filterPathsByHash(obj.src, taskName, targetName, function(err, src) { 247 | if (err) { 248 | return done(err); 249 | } 250 | if (src.length) { 251 | modified = true; 252 | } 253 | done(null, {src: src, dest: obj.dest}); 254 | }); 255 | 256 | }, function(err, newerFiles) { 257 | callback(err, newerFiles, modified); 258 | }); 259 | }; 260 | 261 | 262 | /** 263 | * Get the path to the cached timestamp for a target. 264 | * @param {string} cacheDir Path to cache dir. 265 | * @param {string} taskName Task name. 266 | * @param {string} targetName Target name. 267 | * @return {string} Path to timestamp. 268 | */ 269 | var getStampPath = exports.getStampPath = function(cacheDir, taskName, 270 | targetName) { 271 | return path.join(cacheDir, taskName, targetName, 'timestamp'); 272 | }; 273 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-newer", 3 | "description": "Run Grunt tasks with only those source files modified since the last successful run.", 4 | "version": "1.3.0", 5 | "author": { 6 | "name": "Tim Schaub", 7 | "url": "http://tschaub.net/" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/tschaub/grunt-newer/issues" 11 | }, 12 | "dependencies": { 13 | "async": "^1.5.2", 14 | "rimraf": "^2.5.2" 15 | }, 16 | "devDependencies": { 17 | "chai": "^3.5.0", 18 | "grunt": "1.0.1", 19 | "grunt-cafe-mocha": "0.1.13", 20 | "grunt-cli": "^1.1.0", 21 | "grunt-contrib-clean": "^1.0.0", 22 | "grunt-contrib-jshint": "^1.0.0", 23 | "grunt-contrib-watch": "^1.0.0", 24 | "mock-fs": "^3.8.0", 25 | "tmp": "0.0.31", 26 | "wrench": "1.5.8" 27 | }, 28 | "engines": { 29 | "node": ">= 0.8.0" 30 | }, 31 | "homepage": "https://github.com/tschaub/grunt-newer", 32 | "keywords": [ 33 | "files", 34 | "grunt", 35 | "gruntplugin", 36 | "newer" 37 | ], 38 | "license": "MIT", 39 | "main": "gruntfile.js", 40 | "peerDependencies": { 41 | "grunt": ">=0.4.1" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "git://github.com/tschaub/grunt-newer.git" 46 | }, 47 | "scripts": { 48 | "start": "grunt test watch", 49 | "test": "grunt test" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tasks/newer.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | var async = require('async'); 5 | var rimraf = require('rimraf'); 6 | 7 | var util = require('../lib/util'); 8 | 9 | var counter = 0; 10 | var configCache = {}; 11 | 12 | function cacheConfig(config) { 13 | ++counter; 14 | configCache[counter] = config; 15 | return counter; 16 | } 17 | 18 | function pluckConfig(id) { 19 | if (!configCache.hasOwnProperty(id)) { 20 | throw new Error('Failed to find id in cache'); 21 | } 22 | var config = configCache[id]; 23 | delete configCache[id]; 24 | return config; 25 | } 26 | 27 | function nullOverride(details, include) { 28 | include(false); 29 | } 30 | 31 | function createTask(grunt) { 32 | return function(taskName, targetName) { 33 | var tasks = []; 34 | var prefix = this.name; 35 | if (!targetName) { 36 | if (!grunt.config(taskName)) { 37 | grunt.fatal('The "' + prefix + '" prefix is not supported for aliases'); 38 | return; 39 | } 40 | Object.keys(grunt.config(taskName)).forEach(function(targetName) { 41 | if (!/^_|^options$/.test(targetName)) { 42 | tasks.push(prefix + ':' + taskName + ':' + targetName); 43 | } 44 | }); 45 | return grunt.task.run(tasks); 46 | } 47 | var args = Array.prototype.slice.call(arguments, 2).join(':'); 48 | var options = this.options({ 49 | cache: path.join(__dirname, '..', '.cache'), 50 | override: nullOverride, 51 | tolerance: 0 // allowed difference between src and dst in ms 52 | }); 53 | 54 | // support deprecated timestamps option 55 | if (options.timestamps) { 56 | grunt.log.warn('DEPRECATED OPTION. Use the "cache" option instead'); 57 | options.cache = options.timestamps; 58 | } 59 | 60 | // Sanity check for the tolerance option 61 | if (typeof options.tolerance !== 'number') { 62 | grunt.log.warn('The tolerance value must be a number, ignoring current ' + 63 | 'value'); 64 | options.tolerance = 0; 65 | } 66 | if (options.tolerance < 0) { 67 | grunt.log.warn('A tolerance value of ' + options.tolerance + 68 | ' is invalid'); 69 | options.tolerance = 0; 70 | } 71 | 72 | var done = this.async(); 73 | 74 | var originalConfig = grunt.config.get([taskName, targetName]); 75 | var config = grunt.util._.clone(originalConfig); 76 | 77 | /** 78 | * Special handling for tasks that expect the `files` config to be a string 79 | * or array of string source paths. 80 | */ 81 | var srcFiles = true; 82 | if (typeof config.files === 'string') { 83 | config.src = [config.files]; 84 | delete config.files; 85 | srcFiles = false; 86 | } else if (Array.isArray(config.files) && 87 | typeof config.files[0] === 'string') { 88 | config.src = config.files; 89 | delete config.files; 90 | srcFiles = false; 91 | } 92 | 93 | var stamp = util.getStampPath(options.cache, taskName, targetName); 94 | var previous; 95 | try { 96 | previous = fs.statSync(stamp).mtime; 97 | } catch (err) { 98 | // task has never succeeded before 99 | previous = new Date(0); 100 | } 101 | 102 | function override(filePath, time, include) { 103 | var details = { 104 | task: taskName, 105 | target: targetName, 106 | path: filePath, 107 | time: time 108 | }; 109 | options.override(details, include); 110 | } 111 | 112 | var files = grunt.task.normalizeMultiTaskFiles(config, targetName); 113 | util.filterFilesByTime( 114 | files, previous, options.tolerance, override, function(e, newerFiles) { 115 | if (e) { 116 | return done(e); 117 | } else if (newerFiles.length === 0) { 118 | grunt.log.writeln('No newer files to process.'); 119 | return done(); 120 | } 121 | 122 | /** 123 | * If we started out with only src files in the files config, 124 | * transform the newerFiles array into an array of source files. 125 | */ 126 | if (!srcFiles) { 127 | newerFiles = newerFiles.map(function(obj) { 128 | return obj.src; 129 | }); 130 | } 131 | 132 | // configure target with only newer files 133 | config.files = newerFiles; 134 | delete config.src; 135 | delete config.dest; 136 | grunt.config.set([taskName, targetName], config); 137 | 138 | // because we modified the task config, cache the original 139 | var id = cacheConfig(originalConfig); 140 | 141 | // run the task, and attend to postrun tasks 142 | var qualified = taskName + ':' + targetName; 143 | var tasks = [ 144 | qualified + (args ? ':' + args : ''), 145 | 'newer-postrun:' + qualified + ':' + id + ':' + options.cache 146 | ]; 147 | grunt.task.run(tasks); 148 | 149 | done(); 150 | }); 151 | 152 | }; 153 | } 154 | 155 | 156 | /** @param {Object} grunt Grunt. */ 157 | module.exports = function(grunt) { 158 | 159 | grunt.registerTask( 160 | 'newer', 'Run a task with only those source files that have been ' + 161 | 'modified since the last successful run.', createTask(grunt)); 162 | 163 | var deprecated = 'DEPRECATED TASK. Use the "newer" task instead'; 164 | grunt.registerTask( 165 | 'any-newer', deprecated, function() { 166 | grunt.log.warn(deprecated); 167 | var args = Array.prototype.join.call(arguments, ':'); 168 | grunt.task.run(['newer:' + args]); 169 | }); 170 | 171 | var internal = 'Internal task.'; 172 | grunt.registerTask( 173 | 'newer-postrun', internal, function(taskName, targetName, id, dir) { 174 | 175 | // if dir includes a ':', grunt will split it among multiple args 176 | dir = Array.prototype.slice.call(arguments, 3).join(':'); 177 | grunt.file.write(util.getStampPath(dir, taskName, targetName), 178 | String(Date.now())); 179 | 180 | // reconfigure task with original config 181 | grunt.config.set([taskName, targetName], pluckConfig(id)); 182 | 183 | }); 184 | 185 | var clean = 'Remove cached timestamps.'; 186 | grunt.registerTask( 187 | 'newer-clean', clean, function(taskName, targetName) { 188 | var done = this.async(); 189 | 190 | /** 191 | * This intentionally only works with the default cache dir. If a 192 | * custom cache dir is provided, it is up to the user to keep it clean. 193 | */ 194 | var cacheDir = path.join(__dirname, '..', '.cache'); 195 | if (taskName && targetName) { 196 | cacheDir = util.getStampPath(cacheDir, taskName, targetName); 197 | } else if (taskName) { 198 | cacheDir = path.join(cacheDir, taskName); 199 | } 200 | if (grunt.file.exists(cacheDir)) { 201 | grunt.log.writeln('Cleaning ' + cacheDir); 202 | rimraf(cacheDir, done); 203 | } else { 204 | done(); 205 | } 206 | }); 207 | 208 | }; 209 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "indent": 2, 5 | "latedef": true, 6 | "newcap": true, 7 | "nonew": true, 8 | "quotmark": "single", 9 | "undef": true, 10 | "trailing": true, 11 | "maxlen": 80, 12 | "globals": { 13 | "exports": true, 14 | "before": false, 15 | "beforeEach": false, 16 | "after": false, 17 | "afterEach": false, 18 | "describe": false, 19 | "it": false, 20 | "module": false, 21 | "process": false, 22 | "require": false, 23 | "__dirname": false 24 | } 25 | } -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var cp = require('child_process'); 3 | var fs = require('fs'); 4 | 5 | var chai = require('chai'); 6 | var tmp = require('tmp'); 7 | var wrench = require('wrench'); 8 | 9 | var fixtures = path.join(__dirname, 'integration', 'fixtures'); 10 | var tmpDir = 'tmp'; 11 | 12 | 13 | /** 14 | * Spawn a Grunt process. 15 | * @param {string} dir Directory with gruntfile.js. 16 | * @param {function(Error, Process)} done Callback. 17 | */ 18 | function spawnGrunt(dir, done) { 19 | var gruntfile = path.join(dir, 'gruntfile.js'); 20 | if (!fs.existsSync(gruntfile)) { 21 | done(new Error('Cannot find gruntfile.js: ' + gruntfile)); 22 | } else { 23 | var node = process.argv[0]; 24 | var grunt = process.argv[1]; // assumes grunt drives these tests 25 | var child = cp.spawn(node, [grunt, '--verbose', '--stack'], {cwd: dir}); 26 | done(null, child); 27 | } 28 | } 29 | 30 | 31 | /** 32 | * Set up before running tests. 33 | * @param {string} name Fixture name. 34 | * @param {function} done Callback. 35 | */ 36 | function cloneFixture(name, done) { 37 | var fixture = path.join(fixtures, name); 38 | if (!fs.existsSync(tmpDir)) { 39 | fs.mkdirSync(tmpDir); 40 | } 41 | 42 | tmp.dir({dir: tmpDir}, function(error, dir) { 43 | if (error) { 44 | return done(error); 45 | } 46 | var scratch = path.join(dir, name); 47 | wrench.copyDirRecursive(fixture, scratch, function(error) { 48 | done(error, scratch); 49 | }); 50 | }); 51 | } 52 | 53 | 54 | /** 55 | * Clone a fixture and run the default Grunt task in it. 56 | * @param {string} name Fixture name. 57 | * @param {function(Error, scratch)} done Called with an error if the task 58 | * fails. Called with the cloned fixture directory if the task succeeds. 59 | */ 60 | exports.buildFixture = function(name, done) { 61 | cloneFixture(name, function(error, scratch) { 62 | if (error) { 63 | return done(error); 64 | } 65 | spawnGrunt(scratch, function(error, child) { 66 | if (error) { 67 | return done(error); 68 | } 69 | var messages = []; 70 | child.stderr.on('data', function(chunk) { 71 | messages.push(chunk.toString()); 72 | }); 73 | child.stdout.on('data', function(chunk) { 74 | messages.push(chunk.toString()); 75 | }); 76 | child.on('close', function(code) { 77 | if (code !== 0) { 78 | done(new Error('Task failed: ' + messages.join(''))); 79 | } else { 80 | done(null, scratch); 81 | } 82 | }); 83 | }); 84 | }); 85 | }; 86 | 87 | 88 | /** 89 | * Clean up after running tests. 90 | * @param {string} scratch Path to scratch directory. 91 | * @param {function} done Callback. 92 | */ 93 | exports.afterFixture = function(scratch, done) { 94 | var error; 95 | if (scratch) { 96 | try { 97 | wrench.rmdirSyncRecursive(scratch, false); 98 | wrench.rmdirSyncRecursive(tmpDir, false); 99 | } catch (err) { 100 | error = err; 101 | } 102 | } 103 | done(error); 104 | }; 105 | 106 | 107 | /** @type {boolean} */ 108 | chai.config.includeStack = true; 109 | 110 | 111 | /** 112 | * Chai's assert function configured to include stacks on failure. 113 | * @type {function} 114 | */ 115 | exports.assert = chai.assert; 116 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-clean-dest/gruntfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | 4 | /** 5 | * @param {Object} grunt Grunt. 6 | */ 7 | module.exports = function(grunt) { 8 | 9 | var log = []; 10 | 11 | grunt.initConfig({ 12 | newer: { 13 | options: { 14 | cache: path.join(__dirname, '.cache') 15 | } 16 | }, 17 | clean: { 18 | one: 'dest/one.js', 19 | all: 'dest' 20 | }, 21 | modified: { 22 | one: { 23 | files: [{ 24 | expand: true, 25 | cwd: 'src/', 26 | src: 'one.coffee', 27 | dest: 'dest/', 28 | ext: '.js' 29 | }] 30 | }, 31 | all: { 32 | files: [{ 33 | expand: true, 34 | cwd: 'src/', 35 | src: '**/*.coffee', 36 | dest: 'dest/', 37 | ext: '.js' 38 | }] 39 | }, 40 | none: { 41 | src: [] 42 | } 43 | }, 44 | log: { 45 | all: { 46 | files: [{ 47 | expand: true, 48 | cwd: 'src/', 49 | src: '**/*.coffee', 50 | dest: 'dest/', 51 | ext: '.js' 52 | }], 53 | getLog: function() { 54 | return log; 55 | } 56 | } 57 | }, 58 | assert: { 59 | that: { 60 | getLog: function() { 61 | return log; 62 | } 63 | } 64 | } 65 | }); 66 | 67 | grunt.loadTasks('../../../node_modules/grunt-contrib-clean/tasks'); 68 | 69 | grunt.loadTasks('../../../tasks'); 70 | grunt.loadTasks('../../../test/integration/tasks'); 71 | 72 | grunt.registerTask('default', function() { 73 | 74 | grunt.task.run([ 75 | // run the log task with newer, expect all files 76 | 'newer:log', 77 | 'assert:that:modified:all', 78 | 79 | // HFS+ filesystem mtime resolution 80 | 'wait:1001', 81 | 82 | // modify one file 83 | 'modified:one', 84 | 85 | // run assert task again, expect one file 86 | 'newer:log', 87 | 'assert:that:modified:one', 88 | 89 | // HFS+ filesystem mtime resolution 90 | 'wait:1001', 91 | 92 | // modify nothing, expect no files 93 | 'newer:log', 94 | 'assert:that:modified:none', 95 | 96 | // remove one dest file, expect one file 97 | 'clean:one', 98 | 'newer:log', 99 | 'assert:that:modified:one', 100 | 101 | // remove all dest file, expect all 102 | 'clean:all', 103 | 'newer:log', 104 | 'assert:that:modified:all' 105 | 106 | ]); 107 | 108 | }); 109 | 110 | }; 111 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-clean-dest/src/one.coffee: -------------------------------------------------------------------------------- 1 | coffee = 2 | is: 'good' 3 | hot: true 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-clean-dest/src/two.coffee: -------------------------------------------------------------------------------- 1 | semicolons = true 2 | coffee = true 3 | semicolons = false if coffee 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-dest/gruntfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | 4 | /** 5 | * @param {Object} grunt Grunt. 6 | */ 7 | module.exports = function(grunt) { 8 | 9 | var log = []; 10 | 11 | grunt.initConfig({ 12 | newer: { 13 | options: { 14 | cache: path.join(__dirname, '.cache') 15 | } 16 | }, 17 | modified: { 18 | one: { 19 | files: [{ 20 | expand: true, 21 | cwd: 'src/', 22 | src: 'one.coffee', 23 | dest: 'dest/', 24 | ext: '.js' 25 | }] 26 | }, 27 | all: { 28 | files: [{ 29 | expand: true, 30 | cwd: 'src/', 31 | src: '**/*.coffee', 32 | dest: 'dest/', 33 | ext: '.js' 34 | }] 35 | }, 36 | none: { 37 | src: [] 38 | } 39 | }, 40 | log: { 41 | all: { 42 | files: [{ 43 | expand: true, 44 | cwd: 'src/', 45 | src: '**/*.coffee', 46 | dest: 'dest/', 47 | ext: '.js' 48 | }], 49 | getLog: function() { 50 | return log; 51 | } 52 | } 53 | }, 54 | assert: { 55 | that: { 56 | getLog: function() { 57 | return log; 58 | } 59 | } 60 | } 61 | }); 62 | 63 | grunt.loadTasks('../../../tasks'); 64 | grunt.loadTasks('../../../test/integration/tasks'); 65 | 66 | grunt.registerTask('default', function() { 67 | 68 | grunt.task.run([ 69 | // run the log task with newer, expect all files 70 | 'newer:log', 71 | 'assert:that:modified:all', 72 | 73 | // HFS+ filesystem mtime resolution 74 | 'wait:1001', 75 | 76 | // modify one file 77 | 'modified:one', 78 | 79 | // run assert task again, expect one file 80 | 'newer:log', 81 | 'assert:that:modified:one', 82 | 83 | // HFS+ filesystem mtime resolution 84 | 'wait:1001', 85 | 86 | // modify nothing, expect no files 87 | 'newer:log', 88 | 'assert:that:modified:none' 89 | 90 | ]); 91 | 92 | }); 93 | 94 | }; 95 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-dest/src/one.coffee: -------------------------------------------------------------------------------- 1 | coffee = 2 | is: 'good' 3 | hot: true 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-dest/src/two.coffee: -------------------------------------------------------------------------------- 1 | semicolons = true 2 | coffee = true 3 | semicolons = false if coffee 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-modify-none/gruntfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | 4 | /** 5 | * @param {Object} grunt Grunt. 6 | */ 7 | module.exports = function(grunt) { 8 | var log = []; 9 | 10 | grunt.initConfig({ 11 | newer: { 12 | options: { 13 | cache: path.join(__dirname, '.cache') 14 | } 15 | }, 16 | modified: { 17 | all: { 18 | src: 'src/**/*.js' 19 | }, 20 | none: { 21 | src: [] 22 | } 23 | }, 24 | log: { 25 | all: { 26 | src: 'src/**/*.js', 27 | getLog: function() { 28 | return log; 29 | } 30 | } 31 | }, 32 | assert: { 33 | that: { 34 | getLog: function() { 35 | return log; 36 | } 37 | } 38 | } 39 | }); 40 | 41 | grunt.loadTasks('../../../tasks'); 42 | grunt.loadTasks('../../../test/integration/tasks'); 43 | 44 | grunt.registerTask('default', function() { 45 | 46 | grunt.task.run([ 47 | // run the task without newer, expect all files 48 | 'log', 49 | 'assert:that:modified:all', 50 | 51 | // run the task with newer, expect all files 52 | 'newer:log', 53 | 'assert:that:modified:all', 54 | 55 | // HFS+ filesystem mtime resolution 56 | 'wait:1001', 57 | 58 | // run the task again without modifying any, expect no files 59 | 'newer:log', 60 | 'assert:that:modified:none' 61 | 62 | ]); 63 | 64 | }); 65 | 66 | }; 67 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-modify-none/src/one.js: -------------------------------------------------------------------------------- 1 | var one = 'one'; 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-modify-none/src/two.js: -------------------------------------------------------------------------------- 1 | var two = 'two'; 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-modify-one/gruntfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | 4 | /** 5 | * @param {Object} grunt Grunt. 6 | */ 7 | module.exports = function(grunt) { 8 | 9 | var log = []; 10 | 11 | grunt.initConfig({ 12 | newer: { 13 | options: { 14 | cache: path.join(__dirname, '.cache') 15 | } 16 | }, 17 | modified: { 18 | one: { 19 | src: 'src/one.js' 20 | }, 21 | all: { 22 | src: 'src/**/*.js' 23 | }, 24 | none: { 25 | src: [] 26 | } 27 | }, 28 | log: { 29 | all: { 30 | src: 'src/**/*.js', 31 | getLog: function() { 32 | return log; 33 | } 34 | } 35 | }, 36 | assert: { 37 | that: { 38 | getLog: function() { 39 | return log; 40 | } 41 | } 42 | } 43 | }); 44 | 45 | grunt.loadTasks('../../../tasks'); 46 | grunt.loadTasks('../../../test/integration/tasks'); 47 | 48 | grunt.registerTask('default', function() { 49 | 50 | grunt.task.run([ 51 | // run the assert task with newer, expect all files 52 | 'newer:log', 53 | 'assert:that:modified:all', 54 | 55 | // HFS+ filesystem mtime resolution 56 | 'wait:1001', 57 | 58 | // modify one file 59 | 'modified:one', 60 | 61 | // run assert task again, expect one file 62 | 'newer:log', 63 | 'assert:that:modified:one', 64 | 65 | // HFS+ filesystem mtime resolution 66 | 'wait:1001', 67 | 68 | // modify nothing, expect no files 69 | 'newer:log', 70 | 'assert:that:modified:none' 71 | 72 | ]); 73 | 74 | }); 75 | 76 | }; 77 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-modify-one/src/one.js: -------------------------------------------------------------------------------- 1 | var one = 'one'; 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-modify-one/src/two.js: -------------------------------------------------------------------------------- 1 | var two = 'two'; 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-override/gruntfile.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var path = require('path'); 3 | 4 | 5 | /** 6 | * @param {Object} grunt Grunt. 7 | */ 8 | module.exports = function(grunt) { 9 | 10 | var log = []; 11 | 12 | grunt.initConfig({ 13 | newer: { 14 | options: { 15 | cache: path.join(__dirname, '.cache'), 16 | override: function(details, include) { 17 | assert.equal(details.task, 'log'); 18 | assert.equal(details.target, 'all'); 19 | assert.equal(typeof details.path, 'string'); 20 | assert(details.time instanceof Date, 'Expected time to be a Date'); 21 | // if called with three.js, include it 22 | if (path.basename(details.path) === 'three.js') { 23 | process.nextTick(function() { 24 | include(true); 25 | }); 26 | } else { 27 | process.nextTick(function() { 28 | include(false); 29 | }); 30 | } 31 | } 32 | } 33 | }, 34 | modified: { 35 | one: { 36 | src: 'src/one.js' 37 | }, 38 | oneThree: { 39 | src: ['src/one.js', 'src/three.js'] 40 | }, 41 | three: { 42 | src: 'src/three.js' 43 | }, 44 | all: { 45 | src: 'src/**/*.js' 46 | }, 47 | none: { 48 | src: [] 49 | } 50 | }, 51 | log: { 52 | all: { 53 | src: 'src/**/*.js', 54 | getLog: function() { 55 | return log; 56 | } 57 | } 58 | }, 59 | assert: { 60 | that: { 61 | getLog: function() { 62 | return log; 63 | } 64 | } 65 | } 66 | }); 67 | 68 | grunt.loadTasks('../../../tasks'); 69 | grunt.loadTasks('../../../test/integration/tasks'); 70 | 71 | grunt.registerTask('default', function() { 72 | 73 | grunt.task.run([ 74 | // run the log task with newer, expect all files 75 | 'newer:log', 76 | 'assert:that:modified:all', 77 | 78 | // HFS+ filesystem mtime resolution 79 | 'wait:1001', 80 | 81 | // modify one file 82 | 'modified:one', 83 | 84 | // run log task again, expect one.js and three.js (due to override) 85 | 'newer:log', 86 | 'assert:that:modified:oneThree', 87 | 88 | // HFS+ filesystem mtime resolution 89 | 'wait:1002', 90 | 91 | // modify nothing, expect three.js (due to override) 92 | 'newer:log', 93 | 'assert:that:modified:three' 94 | 95 | ]); 96 | 97 | }); 98 | 99 | }; 100 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-override/src/one.js: -------------------------------------------------------------------------------- 1 | var one = 'one'; 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-override/src/three.js: -------------------------------------------------------------------------------- 1 | var three = 'three'; 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-override/src/two.js: -------------------------------------------------------------------------------- 1 | var two = 'two'; 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-reconfigure/gruntfile.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var path = require('path'); 3 | 4 | 5 | /** 6 | * @param {Object} grunt Grunt. 7 | */ 8 | module.exports = function(grunt) { 9 | 10 | var log = []; 11 | 12 | grunt.initConfig({ 13 | newer: { 14 | options: { 15 | cache: path.join(__dirname, '.cache') 16 | } 17 | }, 18 | modified: { 19 | one: { 20 | files: [{ 21 | expand: true, 22 | cwd: 'src/', 23 | src: 'one.coffee', 24 | dest: 'dest/', 25 | ext: '.js' 26 | }] 27 | }, 28 | all: { 29 | files: [{ 30 | expand: true, 31 | cwd: 'src/', 32 | src: '**/*.coffee', 33 | dest: 'dest/', 34 | ext: '.js' 35 | }] 36 | }, 37 | none: { 38 | src: [] 39 | } 40 | }, 41 | log: { 42 | all: { 43 | files: [{ 44 | expand: true, 45 | cwd: 'src/', 46 | src: '**/*.coffee', 47 | dest: 'dest/', 48 | ext: '.js' 49 | }], 50 | getLog: function() { 51 | return log; 52 | } 53 | } 54 | }, 55 | assert: { 56 | that: { 57 | getLog: function() { 58 | return log; 59 | } 60 | } 61 | } 62 | }); 63 | 64 | grunt.loadTasks('../../../tasks'); 65 | grunt.loadTasks('../../../test/integration/tasks'); 66 | 67 | grunt.registerTask('assert-reconfigured', function() { 68 | var config = grunt.config.get(['log', 'all']); 69 | assert.deepEqual(Object.keys(config).sort(), ['files', 'getLog']); 70 | var files = config.files; 71 | assert.equal(files.length, 1); 72 | assert.deepEqual(Object.keys(files[0]).sort(), 73 | ['cwd', 'dest', 'expand', 'ext', 'src']); 74 | assert.equal(files[0].src, '**/*.coffee'); 75 | }); 76 | 77 | grunt.registerTask('default', function() { 78 | 79 | grunt.task.run([ 80 | // run the log task with newer, expect all files 81 | 'newer:log', 82 | 'assert:that:modified:all', 83 | 84 | // HFS+ filesystem mtime resolution 85 | 'wait:1001', 86 | 87 | // modify one file 88 | 'modified:one', 89 | 90 | // run assert task again, expect one file 91 | 'newer:log', 92 | 'assert:that:modified:one', 93 | 94 | // check that log:all task has been reconfigured with original config 95 | 'assert-reconfigured' 96 | ]); 97 | 98 | }); 99 | 100 | }; 101 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-reconfigure/src/one.coffee: -------------------------------------------------------------------------------- 1 | coffee = 2 | is: 'good' 3 | hot: true 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-reconfigure/src/two.coffee: -------------------------------------------------------------------------------- 1 | semicolons = true 2 | coffee = true 3 | semicolons = false if coffee 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-tolerance/gruntfile.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var path = require('path'); 3 | 4 | 5 | /** 6 | * @param {Object} grunt Grunt. 7 | */ 8 | module.exports = function(grunt) { 9 | 10 | var log = []; 11 | 12 | grunt.initConfig({ 13 | newer: { 14 | options: { 15 | cache: path.join(__dirname, '.cache'), 16 | tolerance: 2000 17 | } 18 | }, 19 | modified: { 20 | one: { 21 | files: [{ 22 | expand: true, 23 | cwd: 'src/', 24 | src: 'one.coffee', 25 | dest: 'dest/', 26 | ext: '.js' 27 | }] 28 | }, 29 | all: { 30 | files: [{ 31 | expand: true, 32 | cwd: 'src/', 33 | src: '**/*.coffee', 34 | dest: 'dest/', 35 | ext: '.js' 36 | }] 37 | }, 38 | none: { 39 | src: [] 40 | } 41 | }, 42 | log: { 43 | all: { 44 | files: [{ 45 | expand: true, 46 | cwd: 'src/', 47 | src: '**/*.coffee', 48 | dest: 'dest/', 49 | ext: '.js' 50 | }], 51 | getLog: function() { 52 | return log; 53 | } 54 | } 55 | }, 56 | assert: { 57 | that: { 58 | getLog: function() { 59 | return log; 60 | } 61 | } 62 | } 63 | }); 64 | 65 | grunt.loadTasks('../../../tasks'); 66 | grunt.loadTasks('../../../test/integration/tasks'); 67 | 68 | grunt.registerTask('assert-reconfigured', function() { 69 | var config = grunt.config.get(['log', 'all']); 70 | assert.deepEqual(Object.keys(config).sort(), ['files', 'getLog']); 71 | var files = config.files; 72 | assert.equal(files.length, 1); 73 | assert.deepEqual(Object.keys(files[0]).sort(), 74 | ['cwd', 'dest', 'expand', 'ext', 'src']); 75 | assert.equal(files[0].src, '**/*.coffee'); 76 | }); 77 | 78 | grunt.registerTask('default', function() { 79 | 80 | grunt.task.run([ 81 | // run the log task with newer, expect all files 82 | 'newer:log', 83 | 'assert:that:modified:all', 84 | 85 | // HFS+ filesystem mtime resolution 86 | 'wait:1001', 87 | 88 | // modify one file 89 | 'modified:one', 90 | 91 | // run assert task again, expect no files 92 | 'newer:log', 93 | 'assert:that:modified:none', 94 | 95 | // check that log:all task has been reconfigured with original config 96 | 'assert-reconfigured' 97 | ]); 98 | 99 | }); 100 | 101 | }; 102 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-tolerance/src/one.coffee: -------------------------------------------------------------------------------- 1 | coffee = 2 | is: 'good' 3 | hot: true 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/newer-tolerance/src/two.coffee: -------------------------------------------------------------------------------- 1 | semicolons = true 2 | coffee = true 3 | semicolons = false if coffee 4 | -------------------------------------------------------------------------------- /test/integration/newer-clean-dest.spec.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var helper = require('../helper'); 4 | 5 | var name = 'newer-clean-dest'; 6 | var gruntfile = path.join(name, 'gruntfile.js'); 7 | 8 | describe(name, function() { 9 | var fixture; 10 | 11 | it('runs the default task (see ' + gruntfile + ')', function(done) { 12 | this.timeout(6000); 13 | helper.buildFixture(name, function(error, dir) { 14 | fixture = dir; 15 | done(error); 16 | }); 17 | }); 18 | 19 | after(function(done) { 20 | helper.afterFixture(fixture, done); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /test/integration/newer-dest.spec.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var helper = require('../helper'); 4 | 5 | var name = 'newer-dest'; 6 | var gruntfile = path.join(name, 'gruntfile.js'); 7 | 8 | describe(name, function() { 9 | var fixture; 10 | 11 | it('runs the default task (see ' + gruntfile + ')', function(done) { 12 | this.timeout(6000); 13 | helper.buildFixture(name, function(error, dir) { 14 | fixture = dir; 15 | done(error); 16 | }); 17 | }); 18 | 19 | after(function(done) { 20 | helper.afterFixture(fixture, done); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /test/integration/newer-modify-none.spec.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var helper = require('../helper'); 4 | 5 | var name = 'newer-modify-none'; 6 | var gruntfile = path.join(name, 'gruntfile.js'); 7 | 8 | describe(name, function() { 9 | var fixture; 10 | 11 | it('runs the default task (see ' + gruntfile + ')', function(done) { 12 | this.timeout(6000); 13 | helper.buildFixture(name, function(error, dir) { 14 | fixture = dir; 15 | done(error); 16 | }); 17 | }); 18 | 19 | after(function(done) { 20 | helper.afterFixture(fixture, done); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /test/integration/newer-modify-one.spec.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var helper = require('../helper'); 4 | 5 | var name = 'newer-modify-one'; 6 | var gruntfile = path.join(name, 'gruntfile.js'); 7 | 8 | describe(name, function() { 9 | var fixture; 10 | 11 | it('runs the default task (see ' + gruntfile + ')', function(done) { 12 | this.timeout(6000); 13 | helper.buildFixture(name, function(error, dir) { 14 | fixture = dir; 15 | done(error); 16 | }); 17 | }); 18 | 19 | after(function(done) { 20 | helper.afterFixture(fixture, done); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /test/integration/newer-override.spec.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var helper = require('../helper'); 4 | 5 | var name = 'newer-override'; 6 | var gruntfile = path.join(name, 'gruntfile.js'); 7 | 8 | describe(name, function() { 9 | var fixture; 10 | 11 | it('runs the default task (see ' + gruntfile + ')', function(done) { 12 | this.timeout(6000); 13 | helper.buildFixture(name, function(error, dir) { 14 | fixture = dir; 15 | done(error); 16 | }); 17 | }); 18 | 19 | after(function(done) { 20 | helper.afterFixture(fixture, done); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /test/integration/newer-reconfigure.spec.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var helper = require('../helper'); 4 | 5 | var name = 'newer-reconfigure'; 6 | var gruntfile = path.join(name, 'gruntfile.js'); 7 | 8 | describe(name, function() { 9 | var fixture; 10 | 11 | it('runs the default task (see ' + gruntfile + ')', function(done) { 12 | this.timeout(6000); 13 | helper.buildFixture(name, function(error, dir) { 14 | fixture = dir; 15 | done(error); 16 | }); 17 | }); 18 | 19 | after(function(done) { 20 | helper.afterFixture(fixture, done); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /test/integration/newer-tolerance.spec.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var helper = require('../helper'); 4 | 5 | var name = 'newer-tolerance'; 6 | var gruntfile = path.join(name, 'gruntfile.js'); 7 | 8 | describe(name, function() { 9 | var fixture; 10 | 11 | it('runs the default task (see ' + gruntfile + ')', function(done) { 12 | this.timeout(6000); 13 | helper.buildFixture(name, function(error, dir) { 14 | fixture = dir; 15 | done(error); 16 | }); 17 | }); 18 | 19 | after(function(done) { 20 | helper.afterFixture(fixture, done); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /test/integration/tasks/index.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var fs = require('fs'); 3 | 4 | 5 | /** 6 | * Create a clone of the object with just src and dest properties. 7 | * @param {Object} obj Source object. 8 | * @return {Object} Pruned clone. 9 | */ 10 | function prune(obj) { 11 | return { 12 | src: obj.src, 13 | dest: obj.dest 14 | }; 15 | } 16 | 17 | 18 | /** 19 | * Remove files config objects with no src files. 20 | * @param {Array} files Array of files config objects. 21 | * @return {Array} Filtered array of files config objects. 22 | */ 23 | function filter(files) { 24 | return files.map(prune).filter(function(obj) { 25 | return obj.src && obj.src.length > 0; 26 | }); 27 | } 28 | 29 | 30 | /** @param {Object} grunt Grunt. */ 31 | module.exports = function(grunt) { 32 | 33 | grunt.registerMultiTask('assert', function(name, target) { 34 | var config = grunt.config([name, target]); 35 | var expected = filter(grunt.task.normalizeMultiTaskFiles(config, target)); 36 | var log = this.data.getLog(); 37 | 38 | if (expected.length === 0) { 39 | assert.equal(log.length, 0, 'Expected no log entries, got ' + log.length); 40 | } else { 41 | assert.equal(log.length, 1, 'Expected one log entry, got ' + log.length); 42 | var actual = log[0]; 43 | assert.deepEqual(actual, expected); 44 | log.length = 0; 45 | } 46 | }); 47 | 48 | 49 | grunt.registerMultiTask('log', function() { 50 | var files = filter(this.files); 51 | if (files.length > 0) { 52 | this.data.getLog().push(files); 53 | } 54 | // create all dest files 55 | files.forEach(function(obj) { 56 | if (obj.dest) { 57 | grunt.file.write(obj.dest, ''); 58 | } 59 | }); 60 | }); 61 | 62 | 63 | grunt.registerTask('wait', function(delay) { 64 | setTimeout(this.async(), delay); 65 | }); 66 | 67 | 68 | grunt.registerMultiTask('modified', function() { 69 | this.filesSrc.forEach(function(filepath) { 70 | var now = new Date(); 71 | fs.utimesSync(filepath, now, now); 72 | grunt.verbose.writeln('Updating mtime for file: ' + filepath, now); 73 | }); 74 | }); 75 | 76 | }; 77 | -------------------------------------------------------------------------------- /test/lib/util.spec.js: -------------------------------------------------------------------------------- 1 | var mock = require('mock-fs'); 2 | 3 | var assert = require('../helper').assert; 4 | var util = require('../../lib/util'); 5 | 6 | 7 | describe('util', function() { 8 | 9 | function nullOverride(filePath, time, include) { 10 | include(false); 11 | } 12 | 13 | describe('filterPathsByTime()', function() { 14 | 15 | beforeEach(function() { 16 | mock({ 17 | src: { 18 | js: { 19 | 'a.js': mock.file({ 20 | mtime: new Date(100) 21 | }), 22 | 'b.js': mock.file({ 23 | mtime: new Date(200) 24 | }), 25 | 'c.js': mock.file({ 26 | mtime: new Date(300) 27 | }) 28 | } 29 | } 30 | }); 31 | }); 32 | afterEach(mock.restore); 33 | 34 | it('calls callback with files newer than provided time', function(done) { 35 | 36 | var paths = [ 37 | 'src/js/a.js', 38 | 'src/js/b.js', 39 | 'src/js/c.js' 40 | ]; 41 | 42 | util.filterPathsByTime(paths, new Date(150), 0, nullOverride, 43 | function(err, results) { 44 | if (err) { 45 | return done(err); 46 | } 47 | assert.equal(results.length, 2); 48 | assert.deepEqual(results.sort(), ['src/js/b.js', 'src/js/c.js']); 49 | done(); 50 | }); 51 | 52 | }); 53 | 54 | it('calls override with older files and comparison time', function(done) { 55 | 56 | var paths = [ 57 | 'src/js/a.js', 58 | 'src/js/b.js', 59 | 'src/js/c.js' 60 | ]; 61 | 62 | function customOverride(filePath, time, include) { 63 | assert.equal(filePath, 'src/js/a.js'); 64 | assert.equal(time.getTime(), 150); 65 | include(false); 66 | } 67 | 68 | util.filterPathsByTime(paths, new Date(150), 0, customOverride, 69 | function(err, results) { 70 | if (err) { 71 | return done(err); 72 | } 73 | assert.equal(results.length, 2); 74 | assert.deepEqual(results.sort(), ['src/js/b.js', 'src/js/c.js']); 75 | done(); 76 | }); 77 | 78 | }); 79 | 80 | it('allows override to force inclusion of older files', function(done) { 81 | 82 | var paths = [ 83 | 'src/js/a.js', 84 | 'src/js/b.js', 85 | 'src/js/c.js' 86 | ]; 87 | 88 | function customOverride(filePath, time, include) { 89 | assert.equal(filePath, 'src/js/a.js'); 90 | assert.equal(time.getTime(), 150); 91 | include(true); 92 | } 93 | 94 | util.filterPathsByTime(paths, new Date(150), 0, customOverride, 95 | function(err, results) { 96 | if (err) { 97 | return done(err); 98 | } 99 | assert.equal(results.length, 3); 100 | assert.deepEqual(results.sort(), 101 | ['src/js/a.js', 'src/js/b.js', 'src/js/c.js']); 102 | done(); 103 | }); 104 | 105 | }); 106 | 107 | it('calls callback error if file not found', function(done) { 108 | 109 | var paths = [ 110 | 'src/bogus-file.js' 111 | ]; 112 | 113 | util.filterPathsByTime(paths, new Date(150), 0, nullOverride, 114 | function(err, results) { 115 | assert.instanceOf(err, Error); 116 | assert.equal(results, undefined); 117 | done(); 118 | }); 119 | 120 | }); 121 | 122 | it('calls callback with files newer than provided time plus tolerance', 123 | function(done) { 124 | 125 | var paths = [ 126 | 'src/js/a.js', 127 | 'src/js/b.js', 128 | 'src/js/c.js' 129 | ]; 130 | 131 | util.filterPathsByTime(paths, new Date(150), 100, nullOverride, 132 | function(err, results) { 133 | if (err) { 134 | return done(err); 135 | } 136 | assert.equal(results.length, 1); 137 | assert.deepEqual(results, ['src/js/c.js']); 138 | done(); 139 | }); 140 | 141 | }); 142 | 143 | }); 144 | 145 | describe('anyNewer()', function() { 146 | 147 | beforeEach(function() { 148 | mock({ 149 | src: { 150 | js: { 151 | 'a.js': mock.file({ 152 | mtime: new Date(100) 153 | }), 154 | 'b.js': mock.file({ 155 | mtime: new Date(200) 156 | }), 157 | 'c.js': mock.file({ 158 | mtime: new Date(300) 159 | }) 160 | } 161 | } 162 | }); 163 | }); 164 | afterEach(mock.restore); 165 | 166 | var paths = [ 167 | 'src/js/a.js', 168 | 'src/js/b.js', 169 | 'src/js/c.js' 170 | ]; 171 | 172 | it('calls callback with true if any file is newer', function(done) { 173 | util.anyNewer(paths, new Date(250), 0, nullOverride, 174 | function(err, newer) { 175 | if (err) { 176 | return done(err); 177 | } 178 | assert.isTrue(newer); 179 | done(); 180 | }); 181 | }); 182 | 183 | it('does not call override if all files are newer', function(done) { 184 | function override(filePath, time, include) { 185 | done(new Error('Override should not be called')); 186 | } 187 | 188 | util.anyNewer(paths, new Date(1), 0, override, function(err, newer) { 189 | if (err) { 190 | return done(err); 191 | } 192 | assert.isTrue(newer); 193 | done(); 194 | }); 195 | }); 196 | 197 | it('calls callback with false if no files are newer', function(done) { 198 | util.anyNewer(paths, new Date(350), 0, nullOverride, 199 | function(err, newer) { 200 | if (err) { 201 | return done(err); 202 | } 203 | assert.isFalse(newer); 204 | done(); 205 | }); 206 | }); 207 | 208 | it('calls callback with false if no files are provided', function(done) { 209 | util.anyNewer([], new Date(), 0, nullOverride, function(err, newer) { 210 | if (err) { 211 | return done(err); 212 | } 213 | assert.isFalse(newer); 214 | done(); 215 | }); 216 | }); 217 | 218 | it('calls override with older file and time', function(done) { 219 | function override(filePath, time, include) { 220 | assert.equal(filePath, 'src/js/a.js'); 221 | assert.equal(time.getTime(), 150); 222 | include(false); 223 | } 224 | 225 | util.anyNewer(paths, new Date(150), 0, override, function(err, newer) { 226 | if (err) { 227 | return done(err); 228 | } 229 | assert.isTrue(newer); 230 | done(); 231 | }); 232 | }); 233 | 234 | it('allows override to force inclusion of older files', function(done) { 235 | function override(filePath, time, include) { 236 | include(true); 237 | } 238 | 239 | util.anyNewer(paths, new Date(1000), 0, override, function(err, newer) { 240 | if (err) { 241 | return done(err); 242 | } 243 | assert.isTrue(newer); 244 | done(); 245 | }); 246 | }); 247 | 248 | it('calls callback with error if file not found', function(done) { 249 | util.anyNewer(['bogus/file.js'], new Date(350), 0, nullOverride, 250 | function(err, newer) { 251 | assert.instanceOf(err, Error); 252 | assert.equal(newer, undefined); 253 | done(); 254 | }); 255 | }); 256 | 257 | it('calls callback with false if files are newer than date plus tolerance', 258 | function(done) { 259 | util.anyNewer(paths, new Date(10), 400, nullOverride, 260 | function(err, newer) { 261 | if (err) { 262 | return done(err); 263 | } 264 | assert.isFalse(newer); 265 | done(); 266 | }); 267 | }); 268 | 269 | }); 270 | 271 | describe('filterFilesByTime()', function() { 272 | 273 | beforeEach(function() { 274 | mock({ 275 | src: { 276 | js: { 277 | 'a.js': mock.file({ 278 | mtime: new Date(100) 279 | }), 280 | 'b.js': mock.file({ 281 | mtime: new Date(200) 282 | }), 283 | 'c.js': mock.file({ 284 | mtime: new Date(300) 285 | }) 286 | }, 287 | less: { 288 | 'one.less': mock.file({mtime: new Date(100)}), 289 | 'two.less': mock.file({mtime: new Date(200)}) 290 | } 291 | }, 292 | dest: { 293 | js: { 294 | 'abc.min.js': mock.file({ 295 | mtime: new Date(200) 296 | }) 297 | }, 298 | css: { 299 | 'one.css': mock.file({mtime: new Date(100)}), 300 | 'two.css': mock.file({mtime: new Date(150)}) 301 | } 302 | } 303 | }); 304 | }); 305 | afterEach(mock.restore); 306 | 307 | it('compares to previous time if src & dest are same (a)', function(done) { 308 | var files = [{ 309 | src: ['src/js/a.js'], 310 | dest: 'src/js/a.js' 311 | }]; 312 | util.filterFilesByTime(files, new Date(50), 0, nullOverride, 313 | function(err, results) { 314 | assert.isNull(err); 315 | assert.equal(results.length, 1); 316 | var result = results[0]; 317 | assert.equal(result.dest, 'src/js/a.js'); 318 | assert.deepEqual(result.src, files[0].src); 319 | done(); 320 | }); 321 | }); 322 | 323 | it('compares to previous time if src & dest are same (b)', function(done) { 324 | var files = [{ 325 | src: ['src/js/a.js'], 326 | dest: 'src/js/a.js' 327 | }]; 328 | util.filterFilesByTime(files, new Date(150), 0, nullOverride, 329 | function(err, results) { 330 | assert.isNull(err); 331 | assert.equal(results.length, 0); 332 | done(); 333 | }); 334 | }); 335 | 336 | it('compares to previous time if src & dest are same (c)', function(done) { 337 | var files = [{ 338 | src: ['src/js/a.js'], 339 | dest: 'src/js/a.js' 340 | }, { 341 | src: ['src/js/b.js'], 342 | dest: 'src/js/b.js' 343 | }]; 344 | util.filterFilesByTime(files, new Date(50), 0, nullOverride, 345 | function(err, results) { 346 | assert.isNull(err); 347 | assert.equal(results.length, 2); 348 | var first = results[0]; 349 | assert.equal(first.dest, 'src/js/a.js'); 350 | assert.deepEqual(first.src, files[0].src); 351 | var second = results[1]; 352 | assert.equal(second.dest, 'src/js/b.js'); 353 | assert.deepEqual(second.src, files[1].src); 354 | done(); 355 | }); 356 | }); 357 | 358 | it('provides all files if any is newer than dest', function(done) { 359 | var files = [{ 360 | src: ['src/js/a.js', 'src/js/b.js', 'src/js/c.js'], 361 | dest: 'dest/js/abc.min.js' 362 | }]; 363 | util.filterFilesByTime(files, new Date(1000), 0, nullOverride, 364 | function(err, results) { 365 | assert.isNull(err); 366 | assert.equal(results.length, 1); 367 | var result = results[0]; 368 | assert.equal(result.dest, 'dest/js/abc.min.js'); 369 | assert.equal(result.src.length, 3); 370 | assert.deepEqual(result.src.sort(), files[0].src); 371 | done(); 372 | }); 373 | }); 374 | 375 | it('provides all files if dest does not exist', function(done) { 376 | var files = [{ 377 | src: ['src/js/a.js', 'src/js/b.js', 'src/js/c.js'], 378 | dest: 'dest/js/foo.min.js' 379 | }]; 380 | util.filterFilesByTime(files, new Date(1000), 0, nullOverride, 381 | function(err, results) { 382 | assert.isNull(err); 383 | assert.equal(results.length, 1); 384 | var result = results[0]; 385 | assert.equal(result.dest, 'dest/js/foo.min.js'); 386 | assert.equal(result.src.length, 3); 387 | assert.deepEqual(result.src.sort(), files[0].src); 388 | done(); 389 | }); 390 | }); 391 | 392 | it('provides newer src files if same as dest', function(done) { 393 | var files = [{ 394 | src: ['src/js/a.js'], 395 | dest: 'src/js/a.js' 396 | }, { 397 | src: ['src/js/b.js'], 398 | dest: 'src/js/b.js' 399 | }, { 400 | src: ['src/js/c.js'], 401 | dest: 'src/js/c.js' 402 | }]; 403 | util.filterFilesByTime(files, new Date(150), 0, nullOverride, 404 | function(err, results) { 405 | assert.isNull(err); 406 | assert.equal(results.length, 2); 407 | var first = results[0]; 408 | assert.equal(first.dest, 'src/js/b.js'); 409 | assert.equal(first.src.length, 1); 410 | assert.deepEqual(first.src, files[1].src); 411 | var second = results[1]; 412 | assert.equal(second.dest, 'src/js/c.js'); 413 | assert.equal(second.src.length, 1); 414 | assert.deepEqual(second.src, files[2].src); 415 | done(); 416 | }); 417 | }); 418 | 419 | it('provides files newer than previous if no dest', function(done) { 420 | var files = [{ 421 | src: ['src/js/a.js', 'src/js/b.js', 'src/js/c.js'] 422 | }]; 423 | util.filterFilesByTime(files, new Date(200), 0, nullOverride, 424 | function(err, results) { 425 | assert.isNull(err); 426 | assert.equal(results.length, 1); 427 | var result = results[0]; 428 | assert.isUndefined(result.dest); 429 | assert.deepEqual(result.src, ['src/js/c.js']); 430 | done(); 431 | }); 432 | }); 433 | 434 | it('provides only newer files for multiple file sets', function(done) { 435 | var files = [{ 436 | src: ['src/less/one.less'], 437 | dest: 'dest/css/one.css' 438 | }, { 439 | src: ['src/less/two.less'], 440 | dest: 'dest/css/two.css' 441 | }]; 442 | util.filterFilesByTime(files, new Date(1000), 0, nullOverride, 443 | function(err, results) { 444 | assert.isNull(err); 445 | assert.equal(results.length, 1); 446 | var result = results[0]; 447 | assert.equal(result.dest, 'dest/css/two.css'); 448 | assert.deepEqual(result.src, ['src/less/two.less']); 449 | done(); 450 | }); 451 | }); 452 | 453 | it('provides an error for a bogus src path', function(done) { 454 | var files = [{ 455 | src: ['src/less/bogus.less'], 456 | dest: 'dest/css/one.css' 457 | }]; 458 | util.filterFilesByTime(files, new Date(1000), 0, nullOverride, 459 | function(err, results) { 460 | assert.instanceOf(err, Error); 461 | assert.isUndefined(results); 462 | done(); 463 | }); 464 | }); 465 | 466 | }); 467 | 468 | }); 469 | --------------------------------------------------------------------------------