Repository: GoogleChrome/sw-precache (js)

Issue:
`.write()` convenience function
The use of this module in WSK is currently a bit [verbose](https://github.com/google/web-starter-kit/blob/7ab4b8b39592b825bc2b24f920e27447cce990f9/gulpfile.js#L218-L254). Would be nice to be able to simplify it a bit. First I think this module should have a convenience method to write the file to disk.


From:

```js
var rootDir = 'dist';

swPrecache({
  cacheId: packageJson.name || 'web-starter-kit',
  dynamicUrlToDependencies: {
    './': [path.join(rootDir, 'index.html')]
  },
  staticFileGlobs: [
    rootDir + '/fonts/**/*.woff',
    rootDir + '/images/**/*',
    rootDir + '/scripts/**/*.js',
    rootDir + '/styles/**/*.css',
    rootDir + '/*.{html,json}'
  ],
  stripPrefix: path.join(rootDir, path.sep)
}, function (error, serviceWorkerFileContents) {
  if (error) {
    return callback(error);
  }
  fs.writeFile(path.join(rootDir, 'service-worker.js'),
    serviceWorkerFileContents, function (error) {
    if (error) {
      return callback(error);
    }
    callback();
  });
});
```

To:

```js
var rootDir = 'dist';

swPrecache.write('service-worker.js', {
  rootDir: 'dist',
  cacheId: packageJson.name || 'web-starter-kit',
  dynamicUrlToDependencies: {
    './': [path.join(rootDir, 'index.html')]
  },
  staticFileGlobs: [
    rootDir + '/fonts/**/*.woff',
    rootDir + '/images/**/*',
    rootDir + '/scripts/**/*.js',
    rootDir + '/styles/**/*.css',
    rootDir + '/*.{html,json}'
  ],
  stripPrefix: path.join(rootDir, path.sep)
}, function () {});
```

And since we specify a `rootDir` option, we might be able to get rid of the `stripPrefix` option usage and the `rootDir` thing in `staticFileGlobs`.

Pull request description:
Bumps to 2.0.0 with a new write() helper
@sindresorhus, I believe this closes #27. (And CC: @addyosmani and @wibblymat)

It amounts to changed the public interface for the module, so I'm bumping the version up to 2.0.0.

General node module best practices feedback is welcomed, as usual.

Patch:
diff --git a/README.md b/README.md
index 37e5dab..8454ca5 100644
--- a/README.md
+++ b/README.md
@@ -56,15 +56,10 @@ Here's a simpler example for a basic use case. It assumes your site's resources
       var swPrecache = require('sw-precache');
       var rootDir = 'app';
 
-      swPrecache({
+      swPrecache.write(path.join(rootDir, 'service-worker.js'), {
         staticFileGlobs: [rootDir + '/**/*.{js,html,css,png,jpg,gif}'],
         stripPrefix: rootDir
-      }, function(error, swFileContents) {
-        if (error) {
-          return callback(error);
-        }
-        fs.writeFile(path.join(rootDir, 'service-worker.js'), swFileContents, callback);
-      });
+      }, callback);
     });
 
 This task will create `app/service-worker.js`, which you'll need to
@@ -94,9 +89,34 @@ can be used in conjunction with `sw-precache` to provide the best experience for
 do implement additional caching logic, put the code in a separate JavaScript file and include it
 using the `importScripts` option.
 
+- `sw-precache` uses a [cache-first](http://jakearchibald.com/2014/offline-cookbook/#cache-falling-back-to-network)
+strategy, which results in a copy of any cached content being returned without consulting the
+network. A useful pattern to adopt is to display a toast/alert to your users when there's new
+content available, and give them an opportunity to reload the page to pick up that new content
+(which the service worker will have  added to the cache, and will be available at the next page
+load). The sample service-worker-registration.js file
+[illustrates](https://github.com/GoogleChrome/sw-precache/blob/7688ee8ccdaddd9171af352384d04d16d712f9d3/demo/app/js/service-worker-registration.js#L51)
+the service worker lifecycle event you can listen for to trigger this message.
+
+
+## Methods
+
+The `sw-precache` module exposes two methods: `generate` and `write`.
+
+### generate(options, callback)
+`generate` takes in [options](#options), generates resulting service worker code as a string, and
+then invokes `callback(error, serviceWorkerString)`.
+In the 1.x releases of `sw-precache`, this was the default and only method exposed by the module.
+
+### write(filePath, options, callback)
+`write` is a helper method that calls `generate` and takes the resulting string content and
+writes it to disk, at `filePath`. It then invokes `callback(error)`.
+
 
 ## Options
 
+Both the `generate()` and `write()` methods take the same options.
+
 ### cacheId [String]
 A string used to distinguish the caches created by different web applications that are served off
 of the same origin and path. While serving completely different sites from the same URL is not
diff --git a/demo/Gruntfile.js b/demo/Gruntfile.js
index eccfe20..4a36309 100644
--- a/demo/Gruntfile.js
+++ b/demo/Gruntfile.js
@@ -17,11 +17,10 @@ module.exports = function(grunt) {
     }
   });
 
-  function generateServiceWorkerFileContents(rootDir, handleFetch, callback) {
+  function writeServiceWorkerFile(rootDir, handleFetch, callback) {
     var config = {
       cacheId: packageJson.name,
       dynamicUrlToDependencies: {
-        './': [path.join(rootDir, 'index.html')],
         'dynamic/page1': [
           path.join(rootDir, 'views', 'layout.jade'),
           path.join(rootDir, 'views', 'page1.jade')
@@ -43,10 +42,12 @@ module.exports = function(grunt) {
         rootDir + '/images/**.*',
         rootDir + '/js/**.js'
       ],
-      stripPrefix: rootDir + '/'
+      stripPrefix: rootDir + '/',
+      // verbose defaults to false, but for the purposes of this demo, log more.
+      verbose: true
     };
 
-    swPrecache(config, callback);
+    swPrecache.write(path.join(rootDir, 'service-worker.js'), config, callback);
   }
 
   grunt.registerMultiTask('swPrecache', function() {
@@ -54,16 +55,11 @@ module.exports = function(grunt) {
     var rootDir = this.data.rootDir;
     var handleFetch = this.data.handleFetch;
 
-    generateServiceWorkerFileContents(rootDir, handleFetch, function(error, serviceWorkerFileContents) {
+    writeServiceWorkerFile(rootDir, handleFetch, function(error) {
       if (error) {
         grunt.fail.warn(error);
       }
-      fs.writeFile(path.join(rootDir, 'service-worker.js'), serviceWorkerFileContents, function(error) {
-        if (error) {
-          grunt.fail.warn(error);
-        }
-        done();
-      });
+      done();
     });
   });
 };
diff --git a/demo/gulpfile.js b/demo/gulpfile.js
index 2395dce..dc32983 100644
--- a/demo/gulpfile.js
+++ b/demo/gulpfile.js
@@ -32,7 +32,7 @@ function runExpress(port, rootDir) {
   });
 }
 
-function generateServiceWorkerFileContents(rootDir, handleFetch, callback) {
+function writeServiceWorkerFile(rootDir, handleFetch, callback) {
   var config = {
     cacheId: packageJson.name,
     dynamicUrlToDependencies: {
@@ -62,7 +62,7 @@ function generateServiceWorkerFileContents(rootDir, handleFetch, callback) {
     verbose: true
   };
 
-  swPrecache(config, callback);
+  swPrecache.write(path.join(rootDir, 'service-worker.js'), config, callback);
 }
 
 gulp.task('default', ['serve-dist']);
@@ -88,21 +88,11 @@ gulp.task('gh-pages', ['build'], function(callback) {
 });
 
 gulp.task('generate-service-worker-dev', function(callback) {
-  generateServiceWorkerFileContents(DEV_DIR, false, function(error, serviceWorkerFileContents) {
-    if (error) {
-      return callback(error);
-    }
-    fs.writeFile(path.join(DEV_DIR, 'service-worker.js'), serviceWorkerFileContents, callback);
-  });
+  writeServiceWorkerFile(DEV_DIR, false, callback);
 });
 
 gulp.task('generate-service-worker-dist', function(callback) {
-  generateServiceWorkerFileContents(DIST_DIR, true, function(error, serviceWorkerFileContents) {
-    if (error) {
-      return callback(error);
-    }
-    fs.writeFile(path.join(DIST_DIR, 'service-worker.js'), serviceWorkerFileContents, callback);
-  });
+  writeServiceWorkerFile(DIST_DIR, true, callback);
 });
 
 gulp.task('copy-dev-to-dist', function() {
diff --git a/lib/sw-precache.js b/lib/sw-precache.js
index 240a81a..16eff4a 100644
--- a/lib/sw-precache.js
+++ b/lib/sw-precache.js
@@ -59,7 +59,7 @@ function getHash(data) {
   return md5.digest('hex');
 }
 
-module.exports = function(params, callback) {
+function generate(params, callback) {
   params = defaults(params || {}, {
     cacheId: '',
     directoryIndex: 'index.html',
@@ -147,8 +147,7 @@ module.exports = function(params, callback) {
 
   fs.readFile(params.templateFilePath, 'utf8', function(error, data) {
     if (error) {
-      callback(error);
-      return;
+      return callback(error);
     }
 
     var populatedTemplate = template(data)({
@@ -165,4 +164,19 @@ module.exports = function(params, callback) {
 
     callback(null, populatedTemplate);
   });
+}
+
+function write(filePath, params, callback) {
+  generate(params, function(error, serviceWorkerFileContents) {
+    if (error) {
+      return callback(error);
+    }
+
+    fs.writeFile(filePath, serviceWorkerFileContents, callback);
+  });
+}
+
+module.exports = {
+  generate: generate,
+  write: write
 };
diff --git a/package.json b/package.json
index eaca697..8669abe 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "sw-precache",
-  "version": "1.3.2",
+  "version": "2.0.0",
   "description": "Generate service worker code that will precache specific resources.",
   "author": {
     "name": "Jeff Posnick",
@@ -20,24 +20,24 @@
     "node": ">=0.10.0"
   },
   "devDependencies": {
-    "del": "^1.1.1",
-    "express": "^4.10.7",
-    "gh-pages": "^0.2.0",
+    "del": "^1.2.0",
+    "express": "^4.12.4",
+    "gh-pages": "^0.3.1",
     "grunt": "^0.4.5",
-    "gulp": "^3.8.10",
-    "gulp-load-plugins": "^0.8.0",
-    "gulp-mocha": "^2.0.0",
-    "gulp-util": "^3.0.1",
-    "jade": "^1.8.2",
-    "mocha": "^2.1.0",
-    "run-sequence": "^1.0.2"
+    "gulp": "^3.9.0",
+    "gulp-load-plugins": "^0.10.0",
+    "gulp-mocha": "^2.1.1",
+    "gulp-util": "^3.0.5",
+    "jade": "^1.11.0",
+    "mocha": "^2.2.5",
+    "run-sequence": "^1.1.0"
   },
   "dependencies": {
-    "dom-urls": "^0.1.1",
-    "glob": "^4.3.2",
-    "lodash.defaults": "^3.0.0",
-    "lodash.template": "^3.0.0",
-    "pretty-bytes": "^1.0.2"
+    "dom-urls": "^1.0.0",
+    "glob": "^5.0.10",
+    "lodash.defaults": "^3.1.1",
+    "lodash.template": "^3.6.1",
+    "pretty-bytes": "^2.0.1"
   },
   "repository": "googlechrome/sw-precache",
   "bugs": "https://github.com/googlechrome/sw-precache/issues",


Test patch:
diff --git a/test/test.js b/test/test.js
index 0a853fe..379a72d 100644
--- a/test/test.js
+++ b/test/test.js
@@ -1,19 +1,21 @@
+var URL = require('dom-urls');
 var assert = require('assert');
 var externalFunctions = require('../lib/functions.js');
 var fs = require('fs');
-var swPrecache = require('../lib/sw-precache.js');
-var URL = require('dom-urls');
+var generate = require('../lib/sw-precache.js').generate;
+var write = require('../lib/sw-precache.js').write;
 
 var NOOP = function() {};
-var TEMP_FILE = 'test/data/temp.txt';
 
 describe('sw-precache core functionality', function() {
+  var TEMP_FILE = 'test/data/temp.txt';
+
   before(function() {
     fs.writeFileSync(TEMP_FILE, 'initial data');
   });
   
   it('should produce valid JavaScript', function(done) {
-    swPrecache({logger: NOOP}, function(error, responseString) {
+    generate({logger: NOOP}, function(error, responseString) {
       assert.ifError(error);
       assert.doesNotThrow(function() {
         new Function(responseString);
@@ -28,9 +30,9 @@ describe('sw-precache core functionality', function() {
       staticFileGlobs: ['test/data/one/**']
     };
 
-    swPrecache(config, function(error, responseStringOne) {
+    generate(config, function(error, responseStringOne) {
       assert.ifError(error);
-      swPrecache(config, function(error, responseStringTwo) {
+      generate(config, function(error, responseStringTwo) {
         assert.ifError(error);
         assert.strictEqual(responseStringOne, responseStringTwo);
         done();
@@ -49,9 +51,9 @@ describe('sw-precache core functionality', function() {
       staticFileGlobs: ['test/data/two/**']
     };
 
-    swPrecache(configOne, function(error, responseStringOne) {
+    generate(configOne, function(error, responseStringOne) {
       assert.ifError(error);
-      swPrecache(configTwo, function(error, responseStringTwo) {
+      generate(configTwo, function(error, responseStringTwo) {
         assert.ifError(error);
         assert.notStrictEqual(responseStringOne, responseStringTwo);
         done();
@@ -78,9 +80,9 @@ describe('sw-precache core functionality', function() {
       ]
     };
 
-    swPrecache(config, function(error, reponseString) {
+    generate(config, function(error, reponseString) {
       assert.ifError(error);
-      swPrecache(configPrime, function(error, responseStringPrime) {
+      generate(configPrime, function(error, responseStringPrime) {
         assert.ifError(error);
         assert.strictEqual(reponseString, responseStringPrime);
         done();
@@ -94,10 +96,10 @@ describe('sw-precache core functionality', function() {
       staticFileGlobs: [TEMP_FILE]
     };
 
-    swPrecache(config, function(error, responseString) {
+    generate(config, function(error, responseString) {
       assert.ifError(error);
       fs.appendFileSync(TEMP_FILE, 'new data');
-      swPrecache(config, function(error, responseStringPrime) {
+      generate(config, function(error, responseStringPrime) {
         assert.ifError(error);
         assert.notStrictEqual(responseString, responseStringPrime);
         done();
@@ -110,6 +112,26 @@ describe('sw-precache core functionality', function() {
   });
 });
 
+describe('sw-precache write functionality', function() {
+  var SW_FILE = 'test/data/generated_sw.js';
+
+  it('should write to disk', function(done) {
+    write(SW_FILE, {logger: NOOP}, function(error) {
+      assert.ifError(error);
+      fs.stat(SW_FILE, function(error, stats) {
+        assert.ifError(error);
+        assert(stats.isFile(), 'file exists');
+        assert(stats.size > 0, 'file contains data');
+        done();
+      });
+    });
+  });
+
+  after(function() {
+    fs.unlinkSync(SW_FILE);
+  });
+});
+
 describe('sw-precache parameters', function() {
   it('should exclude files larger than maximumFileSizeToCacheInBytes', function(done) {
     var file = 'test/data/one/a.txt';
@@ -120,10 +142,10 @@ describe('sw-precache parameters', function() {
       maximumFileSizeToCacheInBytes: size - 1
     };
 
-    swPrecache(config, function(error, responseStringSmaller) {
+    generate(config, function(error, responseStringSmaller) {
       assert.ifError(error);
       config.maximumFileSizeToCacheInBytes = size;
-      swPrecache(config, function(error, responseStringLarger) {
+      generate(config, function(error, responseStringLarger) {
         assert.ifError(error);
         assert(responseStringSmaller.length < responseStringLarger.length);
         done();