├── .gitignore ├── .jshintrc ├── .travis.yml ├── AUTHORS ├── Gruntfile.js ├── LICENSE-MIT ├── README.md ├── package.json ├── tasks └── manifest.js └── test ├── expected ├── manifest.appcache ├── manifest1.appcache └── manifest2.appcache ├── fixtures ├── folder_one │ ├── one.css │ └── one.js ├── folder_two │ ├── two.css │ └── two.js ├── master1.html ├── master2.html ├── test.css └── test.js └── manifest_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | tmp 4 | .DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | Icon 8 | .Spotlight-V100 9 | .Trashes 10 | *.iml 11 | *.ipr 12 | *.iws 13 | .idea/ 14 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "boss": true, 11 | "eqnull": true, 12 | "node": true 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | before_install: 5 | - npm install -g grunt-cli 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Gunther Brunner (http://gunta.org) 2 | Saul Hardman (http://saulhardman.com) 3 | muxa 4 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-manifest 3 | * https://github.com/gunta/grunt-manifest 4 | * 5 | * Copyright (c) 2013 Gunther Brunner, contributors 6 | * Licensed under the MIT license. 7 | * https://github.com/gunta/grunt-manifest/blob/master/LICENSE-MIT 8 | */ 9 | 10 | module.exports = function (grunt) { 11 | 'use strict'; 12 | 13 | // Project configuration. 14 | grunt.initConfig({ 15 | jshint: { 16 | all: ['Gruntfile.js', 'tasks/*.js', '<%= nodeunit.tests %>'], 17 | options: { 18 | jshintrc: '.jshintrc' 19 | } 20 | }, 21 | 22 | // Before generating any new files, remove any previously-created files. 23 | clean: { 24 | test: ['tmp'] 25 | }, 26 | 27 | // Configuration to be run (and then tested). 28 | manifest: { 29 | generate: { 30 | options: { 31 | basePath: 'test/fixtures', 32 | timestamp: false, 33 | hash: true 34 | }, 35 | src: [ 36 | '*.js', 37 | '*.css', 38 | 'folder_one/*', 39 | 'folder_two/*.js', 40 | 'folder_two/*.css' 41 | ], 42 | dest: 'tmp/manifest.appcache' 43 | }, 44 | master1: { 45 | options: { 46 | basePath: 'test/fixtures', 47 | timestamp: false, 48 | hash: true, 49 | master: 'master1.html' 50 | }, 51 | src: [ 52 | '*.js', 53 | '*.css', 54 | 'folder_one/*', 55 | 'folder_two/*.js', 56 | 'folder_two/*.css' 57 | ], 58 | dest: 'tmp/manifest1.appcache' 59 | }, 60 | master1and2: { 61 | options: { 62 | basePath: 'test/fixtures', 63 | timestamp: false, 64 | hash: true, 65 | master: ['master1.html', 'master2.html'] 66 | }, 67 | src: [ 68 | '*.js', 69 | '*.css', 70 | 'folder_one/*', 71 | 'folder_two/*.js', 72 | 'folder_two/*.css' 73 | ], 74 | dest: 'tmp/manifest2.appcache' 75 | } 76 | }, 77 | 78 | // Unit tests. 79 | nodeunit: { 80 | tests: ['test/*_test.js'] 81 | } 82 | }); 83 | 84 | // Actually load this plugin's task(s). 85 | grunt.loadTasks('tasks'); 86 | 87 | // The clean plugin helps in testing. 88 | grunt.loadNpmTasks('grunt-contrib-jshint'); 89 | grunt.loadNpmTasks('grunt-contrib-clean'); 90 | grunt.loadNpmTasks('grunt-contrib-nodeunit'); 91 | 92 | // Whenever the 'test' task is run, first clean the 'tmp' dir, then run this 93 | // plugin's task(s), then test the result. 94 | grunt.registerTask('test', ['clean', 'manifest', 'nodeunit', 'clean']); 95 | 96 | // By default, lint and run all tests. 97 | grunt.registerTask('default', ['jshint', 'test']); 98 | }; 99 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Gunther Brunner, contributors. 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-manifest [![Build Status](https://travis-ci.org/gunta/grunt-manifest.png)](http://travis-ci.org/gunta/grunt-manifest) 2 | > Generate HTML5 Cache Manifest files. 3 | 4 | 5 | ## Getting Started 6 | This plugin requires Grunt `~0.4.5` 7 | 8 | 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](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: 9 | 10 | ```shell 11 | npm install grunt-manifest --save-dev 12 | ``` 13 | 14 | Once the plugin has been installed, it may be enabled inside your Gruntfile with this line of JavaScript: 15 | 16 | ```js 17 | grunt.loadNpmTasks('grunt-manifest'); 18 | ``` 19 | 20 | 21 | 22 | ### HTML5 Cache Manifest task 23 | 24 | _Run this task with the `grunt manifest` command._ 25 | 26 | Visit the [Appcache Facts](http://appcache.offline.technology/) for more information on Cache Manifest files. 27 | 28 | Task targets, files and options may be specified according to the grunt [Configuring tasks](http://gruntjs.com/configuring-tasks) guide. 29 | 30 | 31 | 32 | ### Parameters 33 | 34 | #### options 35 | Type: `Object` 36 | Default: `{}` 37 | 38 | 39 | This controls how this task (and its helpers) operate and should contain key:value pairs, see options below. 40 | 41 | #### src 42 | Type: `String` `Array` 43 | Default: `undefined` 44 | 45 | Sets the input files. 46 | 47 | #### dest 48 | Type: `String` 49 | Default `manifest.appcache` 50 | 51 | Sets the name of the Cache Manifest file. 52 | Remember that `.appcache` is now the W3C recommended file extension. 53 | 54 | ### Options 55 | 56 | #### basePath 57 | Type: `String` 58 | Default: `undefined` 59 | 60 | Sets the base path for **input files**. **_It's recommended to set this_**. 61 | 62 | #### cache 63 | Type: `String` 64 | Default: `undefined` 65 | 66 | Adds manually a string to the **CACHE** section. Needed when you have cache buster for example. 67 | 68 | #### process 69 | Type: `Function` 70 | Default: `undefined` 71 | 72 | A function to process src files path strings before adding them on the appcache 73 | manifest. 74 | 75 | #### exclude 76 | Type: `String` `Array` 77 | Default: `undefined` 78 | 79 | Exclude specific files from the Cache Manifest file. 80 | 81 | #### network 82 | Type: `String` `Array` 83 | Default: `"*"` (By default, an online whitelist wildcard flag is added) 84 | 85 | Adds a string to the **NETWORK** section. 86 | 87 | See [here](http://diveintohtml5.info/offline.html#network) for more information. 88 | 89 | #### fallback 90 | Type: `String` `Array` 91 | Default: `undefined` 92 | 93 | Adds a string to the **FALLBACK** section. 94 | 95 | See [here](http://diveintohtml5.info/offline.html#fallback) for more information. 96 | 97 | #### preferOnline 98 | Type: `Boolean` 99 | Default: `undefined` 100 | 101 | Adds a string to the **SETTINGS** section, specifically the cache mode flag of the ```prefer-online``` state. 102 | 103 | See [here](http://www.whatwg.org/specs/web-apps/current-work/multipage/offline.html#concept-appcache-mode-prefer-online) for more information. 104 | 105 | #### headcomment 106 | Type: `String` 107 | Default: `undefined` 108 | 109 | Adds ability to append custom text to the top of the manifest file. For example, you can put your app version, so that the manifest only updates when your app version changes. Note: A '#' is added to the beginning of the text for you. 110 | 111 | #### verbose 112 | Type: `Boolean` 113 | Default: `true` 114 | 115 | Adds a meta "copyright" comment. 116 | 117 | #### timestamp 118 | Type: `Boolean` 119 | Default: `true` 120 | 121 | Adds a timestamp as a comment for easy versioning. 122 | 123 | Note: timestamp will invalidate application cache whenever cache manifest is rebuilt, even if contents of files in `src` have not changed. 124 | 125 | #### hash 126 | Type: `Boolean` 127 | Default: `false` 128 | 129 | Adds a sha256 hash of all `src` files (actual contents) as a comment. 130 | 131 | This will ensure that application cache invalidates whenever actual file contents change (it's recommented to set `timestamp` to `false` when `hash` is used). 132 | 133 | #### master 134 | Type: `String` `Array` 135 | Default: `undefined` 136 | 137 | Hashes master html files (used with `hash`). Paths must be relative to the 'basePath'. This is useful when there are multiple html pages using one cache manifest and you don't want to explicitly include those pages in the manifest. 138 | 139 | ### Config Example 140 | 141 | ```js 142 | // Project configuration 143 | grunt.initConfig({ 144 | pkg: grunt.file.readJSON('package.json'), 145 | manifest: { 146 | generate: { 147 | options: { 148 | basePath: '../', 149 | cache: ['js/app.js', 'css/style.css'], 150 | network: ['http://*', 'https://*'], 151 | fallback: ['/ /offline.html'], 152 | exclude: ['js/jquery.min.js'], 153 | preferOnline: true, 154 | headcomment: " <%= pkg.name %> v<%= pkg.version %>", 155 | verbose: true, 156 | timestamp: true, 157 | hash: true, 158 | master: ['index.html'], 159 | process: function(path) { 160 | return path.substring('build/'.length); 161 | } 162 | }, 163 | src: [ 164 | 'build/some_files/*.html', 165 | 'build/js/*.min.js', 166 | 'build/css/*.css' 167 | ], 168 | dest: 'manifest.appcache' 169 | } 170 | } 171 | }); 172 | ``` 173 | 174 | ### Output example 175 | 176 | ``` 177 | CACHE MANIFEST 178 | # APPNAME v1.0.0 179 | # This manifest was generated by grunt-manifest HTML5 Cache Manifest Generator 180 | # Time: Mon Jan 01 2155 22:23:24 GMT+0900 (JST) 181 | 182 | CACHE: 183 | js/app.js 184 | css/style 185 | css/style.css 186 | js/zepto.min.js 187 | js/script.js 188 | some_files/index.html 189 | some_files/about.html 190 | 191 | NETWORK: 192 | * 193 | 194 | # hash: 76f0ef591f999871e1dbdf6d5064d1276d80846feeef6b556f74ad87b44ca16a 195 | ``` 196 | 197 | You do need to be fully aware of standard browser caching. 198 | If the files in **CACHE** are in the network cache, they won't actually update, 199 | since the network cache will spit back the same file to the application cache. 200 | Therefore, it's recommended to add a hash to the filenames's, akin to rails or yeoman. See [here](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/) why query strings are not recommended. 201 | 202 | 203 | 204 | ## Release History 205 | 206 | * 2015/01/26 - v0.4.1 - Documented process for renaming files. 207 | * 2012/10/23 - v0.4.0 - Changed package and repository name to grunt-manifest. 208 | * 2012/10/23 - v0.3.0 - Upgraded to Grunt 0.4. Fixed dependencies. Fixed basePath. 209 | * 2012/10/23 - v0.2.1 - Added possibility to manually specify "CACHE:" files. Made comments optional. 210 | * 2012/09/28 - v0.2.0 - Refactored from grunt-contrib into individual repo. 211 | 212 | 213 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-manifest", 3 | "description": "Generate HTML5 Cache Manifest files", 4 | "version": "0.4.4", 5 | "homepage": "https://github.com/gunta/grunt-manifest", 6 | "author": { 7 | "name": "Gunther Brunner", 8 | "url": "http://gunta.org/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/gunta/grunt-manifest.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/gunta/grunt-manifest/issues" 16 | }, 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/gunta/grunt-manifest/blob/master/LICENSE-MIT" 21 | } 22 | ], 23 | "main": "Gruntfile.js", 24 | "engines": { 25 | "node": ">= 0.10.0" 26 | }, 27 | "scripts": { 28 | "test": "grunt test" 29 | }, 30 | "devDependencies": { 31 | "grunt": "~0.4.5", 32 | "grunt-contrib-clean": "~0.6.0", 33 | "grunt-contrib-nodeunit": "~0.4.1", 34 | "grunt-contrib-jshint": "~0.10.0" 35 | }, 36 | "keywords": [ 37 | "gruntplugin" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /tasks/manifest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-manifest 3 | * https://github.com/gunta/grunt-manifest 4 | * 5 | * Copyright (c) 2015 Gunther Brunner, contributors 6 | * Licensed under the MIT license. 7 | * https://github.com/gunta/grunt-manifest/blob/master/LICENSE-MIT 8 | */ 9 | 10 | 'use strict'; 11 | module.exports = function (grunt) { 12 | 13 | grunt.registerMultiTask('manifest', 'Generate HTML5 cache manifest', function () { 14 | 15 | var options = this.options({verbose: true, timestamp: true}); 16 | var done = this.async(); 17 | 18 | grunt.verbose.writeflags(options, 'Options'); 19 | 20 | var path = require('path'); 21 | 22 | this.files.forEach(function (file) { 23 | 24 | var files; 25 | var cacheFiles = options.cache; 26 | var contents = 'CACHE MANIFEST\n'; 27 | 28 | // if hash options is specified it will be used to calculate 29 | // a hash of local files that are included in the manifest 30 | var hasher; 31 | 32 | if (options.hash) { 33 | hasher = require('crypto').createHash('sha256'); 34 | } 35 | 36 | // check to see if src has been set 37 | if (typeof file.src === "undefined") { 38 | grunt.fatal('Need to specify which files to include in the manifest.', 2); 39 | } 40 | 41 | // if a basePath is set, expand using the original file pattern 42 | if (options.basePath) { 43 | files = grunt.file.expand({cwd: options.basePath}, file.orig.src); 44 | } else { 45 | files = file.src; 46 | } 47 | 48 | // Exclude files 49 | if (options.exclude) { 50 | files = files.filter(function (item) { 51 | return options.exclude.indexOf(item) === -1; 52 | }); 53 | } 54 | 55 | // Set default destination file 56 | if (!file.dest) { 57 | file.dest = 'manifest.appcache'; 58 | } 59 | 60 | if (options.headcomment) { 61 | contents += '#' + options.headcomment + '\n'; 62 | } 63 | 64 | if (options.verbose) { 65 | contents += '# This manifest was generated by grunt-manifest HTML5 Cache Manifest Generator\n'; 66 | } 67 | 68 | if (options.timestamp) { 69 | contents += '# Time: ' + new Date() + '\n'; 70 | } 71 | 72 | if (options.revision) { 73 | contents += '# Revision: ' + options.revision + '\n'; 74 | } 75 | 76 | // Cache section 77 | contents += '\nCACHE:\n'; 78 | 79 | // add files to explicit cache manually 80 | if (cacheFiles) { 81 | cacheFiles.forEach(function (item) { 82 | contents += encodeURI(item) + '\n'; 83 | }); 84 | } 85 | 86 | // add files to explicit cache 87 | if (files) { 88 | files.forEach(function (item) { 89 | if (options.process) { 90 | contents += encodeURI(options.process(item)) + '\n'; 91 | } else { 92 | contents += encodeURI(item) + '\n'; 93 | } 94 | 95 | // hash file contents 96 | if (options.hash) { 97 | grunt.verbose.writeln('Hashing ' + path.join(options.basePath, item)); 98 | hasher.update(grunt.file.read(path.join(options.basePath, item)), 'binary'); 99 | } 100 | }); 101 | } 102 | 103 | // Network section 104 | if (options.network) { 105 | contents += '\nNETWORK:\n'; 106 | options.network.forEach(function (item) { 107 | contents += encodeURI(item) + '\n'; 108 | }); 109 | } else { 110 | // If there's no network section, add a default '*' wildcard 111 | contents += '\nNETWORK:\n'; 112 | contents += '*\n'; 113 | } 114 | 115 | // Fallback section 116 | if (options.fallback) { 117 | contents += '\nFALLBACK:\n'; 118 | options.fallback.forEach(function (item) { 119 | contents += encodeURI(item).replace('%20', ' ') + '\n'; 120 | }); 121 | } 122 | 123 | // Settings section 124 | if (options.preferOnline) { 125 | contents += '\nSETTINGS:\n'; 126 | contents += 'prefer-online\n'; 127 | } 128 | 129 | // output hash to cache manifest 130 | if (options.hash) { 131 | 132 | // hash masters as well 133 | if (options.master) { 134 | 135 | // convert form string to array 136 | if (typeof options.master === 'string') { 137 | options.master = [options.master]; 138 | } 139 | 140 | options.master.forEach(function (item) { 141 | grunt.log.writeln('Hashing ' + path.join(options.basePath, item)); 142 | hasher.update(grunt.file.read(path.join(options.basePath, item)), 'binary'); 143 | }); 144 | } 145 | 146 | contents += '\n# hash: ' + hasher.digest("hex"); 147 | } 148 | 149 | grunt.verbose.writeln('\n' + (contents).yellow); 150 | 151 | grunt.file.write(file.dest, contents); 152 | 153 | grunt.log.writeln('File "' + file.dest + '" created.'); 154 | 155 | done(); 156 | 157 | }); 158 | 159 | }); 160 | 161 | }; 162 | -------------------------------------------------------------------------------- /test/expected/manifest.appcache: -------------------------------------------------------------------------------- 1 | CACHE MANIFEST 2 | # This manifest was generated by grunt-manifest HTML5 Cache Manifest Generator 3 | 4 | CACHE: 5 | test.js 6 | test.css 7 | folder_one/one.css 8 | folder_one/one.js 9 | folder_two/two.js 10 | folder_two/two.css 11 | 12 | NETWORK: 13 | * 14 | 15 | # hash: 76f0ef591f999871e1dbdf6d5064d1276d80846feeef6b556f74ad87b44ca16a -------------------------------------------------------------------------------- /test/expected/manifest1.appcache: -------------------------------------------------------------------------------- 1 | CACHE MANIFEST 2 | # This manifest was generated by grunt-manifest HTML5 Cache Manifest Generator 3 | 4 | CACHE: 5 | test.js 6 | test.css 7 | folder_one/one.css 8 | folder_one/one.js 9 | folder_two/two.js 10 | folder_two/two.css 11 | 12 | NETWORK: 13 | * 14 | 15 | # hash: cbf8d96b35b5371146649d06a3d73c8a0b9747ee0fbf46aacb0f8d04076065d5 -------------------------------------------------------------------------------- /test/expected/manifest2.appcache: -------------------------------------------------------------------------------- 1 | CACHE MANIFEST 2 | # This manifest was generated by grunt-manifest HTML5 Cache Manifest Generator 3 | 4 | CACHE: 5 | test.js 6 | test.css 7 | folder_one/one.css 8 | folder_one/one.js 9 | folder_two/two.js 10 | folder_two/two.css 11 | 12 | NETWORK: 13 | * 14 | 15 | # hash: 7d673b6297931b19b2a57881a83851eeaf006a7533b151c3dbfc368165ba5371 -------------------------------------------------------------------------------- /test/fixtures/folder_one/one.css: -------------------------------------------------------------------------------- 1 | body,td{}a{}a:hover{} -------------------------------------------------------------------------------- /test/fixtures/folder_one/one.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){$.noConflict();}); -------------------------------------------------------------------------------- /test/fixtures/folder_two/two.css: -------------------------------------------------------------------------------- 1 | body,td{color:blue;}a{}a:hover{} -------------------------------------------------------------------------------- /test/fixtures/folder_two/two.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){jQuery}); -------------------------------------------------------------------------------- /test/fixtures/master1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Master 1 7 | 8 | -------------------------------------------------------------------------------- /test/fixtures/master2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Master 2 7 | 8 | -------------------------------------------------------------------------------- /test/fixtures/test.css: -------------------------------------------------------------------------------- 1 | body,td{background:green;}a{}a:hover{} -------------------------------------------------------------------------------- /test/fixtures/test.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){}); -------------------------------------------------------------------------------- /test/manifest_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('grunt'); 4 | 5 | exports.manifest = { 6 | generate: function (test) { 7 | test.expect(3); 8 | 9 | var actual = grunt.file.read('tmp/manifest.appcache'); 10 | var expected = grunt.file.read('test/expected/manifest.appcache'); 11 | 12 | test.equal(actual, expected, 'should generate a cache manifest.'); 13 | 14 | actual = grunt.file.read('tmp/manifest1.appcache'); 15 | expected = grunt.file.read('test/expected/manifest1.appcache'); 16 | 17 | test.equal(actual, expected, 18 | 'should generate a cache manifest (with master1.html as master).'); 19 | 20 | actual = grunt.file.read('tmp/manifest2.appcache'); 21 | expected = grunt.file.read('test/expected/manifest2.appcache'); 22 | 23 | test.equal(actual, expected, 24 | 'should generate a cache manifest (with master1.html & master2.html as masters).'); 25 | 26 | test.done(); 27 | } 28 | }; 29 | --------------------------------------------------------------------------------