├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── README.md ├── lib └── index.js ├── package.json ├── src └── index.coffee └── test ├── common.js └── plugin_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.bak 3 | .DS_Store 4 | npm-debug.log 5 | node_modules/ 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .lock-wscript 2 | .svn/ 3 | .hg/ 4 | .git/ 5 | CVS/ 6 | *~ 7 | *.bak 8 | .DS_Store 9 | npm-debug.log 10 | test/ 11 | src/ 12 | CHANGELOG.md 13 | 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # appcache-brunch 1.7.1 (3 November 2013) 2 | * Added support for manifestFile and externalCacheEntries settings. 3 | 4 | # appcache-brunch 1.7.0 (28 August 2013) 5 | * Complete rewrite to generate appcache based on all files in public directory. 6 | * Keeps track of file changes and updates appcache accordingly 7 | * Massive thanks to @davidchambers 8 | 9 | # appcache-brunch 1.5.1 (19 March 2013) 10 | * Added node 0.10 support, removed coffee-script dependency. 11 | 12 | # appcache-brunch 1.5.0 (13 January 2013) 13 | * Improved installation process. 14 | 15 | # appcache-brunch 1.4.0 (15 July 2012) 16 | * Initial release 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # appcache-brunch 2 | 3 | [Brunch][1] plugin which generates a [cache manifest][2] as part of the 4 | `brunch build` process. 5 | 6 | [1]: http://brunch.io 7 | [2]: https://developer.mozilla.org/en-US/docs/HTML/Using_the_application_cache#The_cache_manifest_file 8 | 9 | ## Usage 10 | 11 | Install the plugin via npm with `npm install --save-dev appcache-brunch`. 12 | 13 | Or, do manual install: 14 | 15 | * Add `"appcache-brunch": "x.y.z"` to `package.json` of your brunch app. 16 | Pick a plugin version that corresponds to your minor (y) brunch version. 17 | * If you want to use git version of plugin, add 18 | `"appcache-brunch": "git+ssh://git@github.com:brunch/appcache-brunch.git"`. 19 | 20 | Specify [plugin settings](#settings) in config.coffee. For example: 21 | 22 | ```coffeescript 23 | exports.config = 24 | # ... 25 | plugins: 26 | appcache: 27 | staticRoot: '/static' 28 | network: ['*'] 29 | fallback: {} 30 | ``` 31 | 32 | Link to the manifest from each template. For example: 33 | 34 | ```html 35 | 36 | ``` 37 | 38 | ## Settings 39 | 40 | ### appcache.staticRoot 41 | 42 | The static media root, such as ".", "/static" or "http://static.example.com". 43 | 44 | Default value : `'.'` 45 | 46 | ### appcache.ignore 47 | 48 | A regular expression specifying paths to omit from the manifest. 49 | 50 | Default value : `/[/][.]/` (hidden files and files in hidden directories are ignored) 51 | 52 | ### appcache.externalCacheEntries 53 | 54 | An array of additionals URIs added to `CACHE` section. For example: 55 | 56 | ```coffeescript 57 | externalCacheEntries: [ 58 | 'http://other.example.org/image.jpg' 59 | # ... 60 | ] 61 | ``` 62 | 63 | Default value : `[]` 64 | 65 | ### appcache.network 66 | 67 | An array of resource URIs which require a network connection added to `NETWORK` section. For example: 68 | 69 | ```coffeescript 70 | network: [ 71 | 'login.php' 72 | '/myapi' 73 | 'http://api.twitter.com' 74 | ] 75 | ``` 76 | 77 | Default value : `["*"]` 78 | 79 | ### appcache.fallback 80 | 81 | An object mapping resource URIs to fallback URIs added to `FALLBACK` section. For example: 82 | 83 | ```coffeescript 84 | fallback: 85 | '/main.py': '/static.html' 86 | 'images/large/': 'images/offline.jpg' 87 | '*.html': '/offline.html' 88 | ``` 89 | 90 | Default value : `{}` 91 | 92 | ### appcache.manifestFile 93 | 94 | Output filename. For example: 95 | 96 | ```coffeescript 97 | manifestFile: "appcache.appcache" 98 | ``` 99 | 100 | Default value : `"appcache.appcache"` 101 | 102 | ## License 103 | 104 | The MIT License (MIT) 105 | 106 | Copyright (c) 2012-2013 Paul Miller (http://paulmillr.com) 107 | 108 | Permission is hereby granted, free of charge, to any person obtaining a copy 109 | of this software and associated documentation files (the "Software"), to deal 110 | in the Software without restriction, including without limitation the rights 111 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 112 | copies of the Software, and to permit persons to whom the Software is 113 | furnished to do so, subject to the following conditions: 114 | 115 | The above copyright notice and this permission notice shall be included in 116 | all copies or substantial portions of the Software. 117 | 118 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 119 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 120 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 121 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 122 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 123 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 124 | THE SOFTWARE. 125 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var Manifest, Walker, crypto, fs, pathlib; 3 | 4 | crypto = require('crypto'); 5 | 6 | fs = require('fs'); 7 | 8 | pathlib = require('path'); 9 | 10 | Walker = (function() { 11 | function Walker() { 12 | this.todo = {}; 13 | this.walking = false; 14 | } 15 | 16 | Walker.prototype.add = function(path) { 17 | this.todo[path] = 1; 18 | return this.walking = true; 19 | }; 20 | 21 | Walker.prototype.del = function(path) { 22 | delete this.todo[path]; 23 | return this.walking = Object.keys(this.todo).length > 0; 24 | }; 25 | 26 | Walker.prototype.readdir = function(path, callback) { 27 | var _this = this; 28 | this.add(path); 29 | return fs.readdir(path, function(err, filenames) { 30 | if (err != null) { 31 | throw err; 32 | } 33 | _this.del(path); 34 | return callback(filenames); 35 | }); 36 | }; 37 | 38 | Walker.prototype.stat = function(path, callback) { 39 | var _this = this; 40 | this.add(path); 41 | return fs.stat(path, function(err, stats) { 42 | if (err != null) { 43 | throw err; 44 | } 45 | _this.del(path); 46 | return callback(stats); 47 | }); 48 | }; 49 | 50 | Walker.prototype.walk = function(path, callback) { 51 | var _this = this; 52 | return this.readdir(path, function(filenames) { 53 | return filenames.forEach(function(filename) { 54 | var filePath; 55 | filePath = pathlib.join(path, filename); 56 | return _this.stat(filePath, function(stats) { 57 | if (stats.isDirectory()) { 58 | return _this.walk(filePath, callback); 59 | } else { 60 | return callback(filePath); 61 | } 62 | }); 63 | }); 64 | }); 65 | }; 66 | 67 | return Walker; 68 | 69 | })(); 70 | 71 | Manifest = (function() { 72 | var format; 73 | 74 | function Manifest(config) { 75 | var cfg, k, _ref, _ref1, _ref2; 76 | this.config = config; 77 | if ('appcache' in this.config) { 78 | console.warn('Warning: config.appcache is deprecated, please move it to config.plugins.appcache'); 79 | } 80 | this.options = { 81 | ignore: /[\\/][.]/, 82 | externalCacheEntries: [], 83 | network: ['*'], 84 | fallback: {}, 85 | staticRoot: '.', 86 | manifestFile: 'appcache.appcache' 87 | }; 88 | cfg = (_ref = (_ref1 = (_ref2 = this.config.plugins) != null ? _ref2.appcache : void 0) != null ? _ref1 : this.config.appcache) != null ? _ref : {}; 89 | for (k in cfg) { 90 | this.options[k] = cfg[k]; 91 | } 92 | } 93 | 94 | Manifest.prototype.brunchPlugin = true; 95 | 96 | Manifest.prototype.onCompile = function() { 97 | var paths, walker, 98 | _this = this; 99 | paths = []; 100 | walker = new Walker; 101 | return walker.walk(this.config.paths["public"], function(path) { 102 | var shasums; 103 | if (!(/[.]appcache$/.test(path) || _this.options.ignore.test(path))) { 104 | paths.push(path); 105 | } 106 | if (!walker.walking) { 107 | shasums = []; 108 | paths.sort(); 109 | return paths.forEach(function(path) { 110 | var s, shasum; 111 | shasum = crypto.createHash('sha1'); 112 | s = fs.ReadStream(path); 113 | s.on('data', function(data) { 114 | return shasum.update(data); 115 | }); 116 | return s.on('end', function() { 117 | var p; 118 | shasums.push(shasum.digest('hex')); 119 | if (shasums.length === paths.length) { 120 | shasum = crypto.createHash('sha1'); 121 | shasum.update(shasums.sort().join(), 'ascii'); 122 | return _this.write((function() { 123 | var _i, _len, _results; 124 | _results = []; 125 | for (_i = 0, _len = paths.length; _i < _len; _i++) { 126 | p = paths[_i]; 127 | _results.push(pathlib.relative(this.config.paths["public"], p)); 128 | } 129 | return _results; 130 | }).call(_this), shasum.digest('hex')); 131 | } 132 | }); 133 | }); 134 | } 135 | }); 136 | }; 137 | 138 | format = function(obj) { 139 | var k; 140 | return ((function() { 141 | var _i, _len, _ref, _results; 142 | _ref = Object.keys(obj).sort(); 143 | _results = []; 144 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 145 | k = _ref[_i]; 146 | _results.push("" + k + " " + obj[k]); 147 | } 148 | return _results; 149 | })()).join('\n'); 150 | }; 151 | 152 | Manifest.prototype.write = function(paths, shasum) { 153 | var p; 154 | return fs.writeFileSync(pathlib.join(this.config.paths["public"], this.options.manifestFile), "CACHE MANIFEST\n# " + shasum + "\n\nNETWORK:\n" + (this.options.network.join('\n')) + "\n\nFALLBACK:\n" + (format(this.options.fallback)) + "\n\nCACHE:\n" + (((function() { 155 | var _i, _len, _results; 156 | _results = []; 157 | for (_i = 0, _len = paths.length; _i < _len; _i++) { 158 | p = paths[_i]; 159 | _results.push("" + this.options.staticRoot + "/" + p); 160 | } 161 | return _results; 162 | }).call(this)).join('\n')) + "\n" + (this.options.externalCacheEntries.join('\n'))); 163 | }; 164 | 165 | return Manifest; 166 | 167 | })(); 168 | 169 | module.exports = Manifest; 170 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appcache-brunch", 3 | "version": "1.7.1", 4 | "description": "Adds HTML5 .appcache generation to brunch.", 5 | "author": "Paul Miller (http://paulmillr.com/)", 6 | "homepage": "https://github.com/brunch/appcache-brunch", 7 | "repository": { 8 | "type": "git", 9 | "url": "git@github.com:brunch/appcache-brunch.git" 10 | }, 11 | "main": "./lib/index", 12 | "scripts": { 13 | "prepublish": "rm -rf lib && coffee --bare --output lib/ src/", 14 | "test": "node_modules/.bin/mocha --require test/common.js" 15 | }, 16 | "dependencies": { }, 17 | "devDependencies": { 18 | "mocha": "1.11.0", 19 | "chai": "1.7.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | crypto = require 'crypto' 2 | fs = require 'fs' 3 | pathlib = require 'path' 4 | 5 | 6 | class Walker 7 | constructor: -> 8 | @todo = {} 9 | @walking = false 10 | 11 | add: (path) -> 12 | @todo[path] = 1 13 | @walking = true 14 | 15 | del: (path) -> 16 | delete @todo[path] 17 | @walking = Object.keys(@todo).length > 0 18 | 19 | readdir: (path, callback) -> 20 | @add path 21 | fs.readdir path, (err, filenames) => 22 | throw err if err? 23 | @del path 24 | callback filenames 25 | 26 | stat: (path, callback) -> 27 | @add path 28 | fs.stat path, (err, stats) => 29 | throw err if err? 30 | @del path 31 | callback stats 32 | 33 | walk: (path, callback) -> 34 | @readdir path, (filenames) => 35 | filenames.forEach (filename) => 36 | filePath = pathlib.join path, filename 37 | @stat filePath, (stats) => 38 | if stats.isDirectory() 39 | @walk filePath, callback 40 | else 41 | callback filePath 42 | 43 | 44 | class Manifest 45 | constructor: (@config) -> 46 | 47 | if 'appcache' of @config 48 | console.warn 'Warning: config.appcache is deprecated, please move it to config.plugins.appcache' 49 | 50 | # Defaults options 51 | @options = { 52 | ignore: /[\\/][.]/ 53 | externalCacheEntries: [] 54 | network: ['*'] 55 | fallback: {} 56 | staticRoot: '.' 57 | manifestFile: 'appcache.appcache' 58 | } 59 | 60 | # Merge config 61 | cfg = @config.plugins?.appcache ? @config.appcache ? {} 62 | @options[k] = cfg[k] for k of cfg 63 | 64 | brunchPlugin: true 65 | 66 | onCompile: -> 67 | paths = [] 68 | walker = new Walker 69 | walker.walk @config.paths.public, (path) => 70 | paths.push path unless /[.]appcache$/.test(path) or @options.ignore.test(path) 71 | unless walker.walking 72 | shasums = [] 73 | paths.sort() 74 | paths.forEach (path) => 75 | shasum = crypto.createHash 'sha1' 76 | s = fs.ReadStream path 77 | s.on 'data', (data) => shasum.update data 78 | s.on 'end', => 79 | shasums.push shasum.digest 'hex' 80 | if shasums.length is paths.length 81 | shasum = crypto.createHash 'sha1' 82 | shasum.update shasums.sort().join(), 'ascii' 83 | @write((pathlib.relative @config.paths.public, p for p in paths).replace /\\/g '/', 84 | shasum.digest 'hex') 85 | 86 | format = (obj) -> 87 | ("#{k} #{obj[k]}" for k in Object.keys(obj).sort()).join('\n') 88 | 89 | write: (paths, shasum) -> 90 | fs.writeFileSync pathlib.join(@config.paths.public, @options.manifestFile), 91 | """ 92 | CACHE MANIFEST 93 | # #{shasum} 94 | 95 | NETWORK: 96 | #{@options.network.join('\n')} 97 | 98 | FALLBACK: 99 | #{format @options.fallback} 100 | 101 | CACHE: 102 | #{("#{@options.staticRoot}/#{p}" for p in paths).join('\n')} 103 | #{@options.externalCacheEntries.join('\n')} 104 | """ 105 | 106 | 107 | module.exports = Manifest 108 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | global.expect = require('chai').expect; 2 | global.Plugin = require('../lib'); 3 | -------------------------------------------------------------------------------- /test/plugin_test.js: -------------------------------------------------------------------------------- 1 | describe('Plugin', function() { 2 | var plugin; 3 | 4 | beforeEach(function() { 5 | plugin = new Plugin({paths: {public: 'public/'}}); 6 | }); 7 | 8 | it('should be an object', function() { 9 | expect(plugin).to.be.ok; 10 | }); 11 | 12 | it('should has #onCompile method', function() { 13 | expect(plugin.onCompile).to.be.an.instanceof(Function); 14 | }); 15 | }); 16 | --------------------------------------------------------------------------------