From 0cfab626fad476f1d66207ea049ca9d923baa9f7 Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" Date: Tue, 11 Apr 2017 20:38:23 -0400 Subject: [PATCH 1/4] Add service worker for offline access to editor --- Gruntfile.js | 104 ++++++++++++++++++++++++++++++++++ package.json | 1 + public/editor/scripts/main.js | 40 +++++++++++++ server/index.js | 3 +- server/security.js | 2 + 5 files changed, 149 insertions(+), 1 deletion(-) diff --git a/Gruntfile.js b/Gruntfile.js index 907e709c3..493d09080 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -8,6 +8,10 @@ module.exports = function( grunt ) { gitadd: 'grunt-git' }); + var swPrecache = require('sw-precache'); + var Path = require('path'); + var fs = require('fs'); + grunt.initConfig({ pkg: grunt.file.readJSON( "package.json" ), @@ -131,7 +135,107 @@ module.exports = function( grunt ) { ] } } + }, + swPrecache: { + dist: { + rootDir: 'dist' + } + } + }); + + grunt.registerMultiTask('swPrecache', function() { + var done = this.async(); + var rootDir = this.data.rootDir; + rootDir = "get rid of this later"; + + // We need the full list of locales so we can create runtime caching rules for each + fs.readdir('locales', function(err, locales) { + if(err) { + grunt.fail.warn(err); + return done(); + } + + locales = locales.map(function(locale) { + // en_US to en-US + return locale.replace('_', '-'); + }); + + // /\/(en-US|pt-BR|es|...)\// + var localesPattern = new RegExp('\/(' + locales.join('|') + ')\/'); + + writeServiceWorker(getConfig(localesPattern)); + }); + + function getConfig(localesPattern) { + return { + cacheId: 'thimble', + logger: grunt.log.writeln, + staticFileGlobs: [ + 'dist/editor/stylesheets/*.css', + 'dist/resources/stylesheets/*.css', + 'dist/homepage/stylesheets/*.css' + ], + runtimeCaching: [ + // TODO: we should be bundling all this vs. loading separate + { + urlPattern: /\/node_modules\//, + handler: 'fastest' + }, + { + urlPattern: /\/scripts\/vendor\//, + handler: 'fastest' + }, + + // TODO: move these to staticFileGlobs--need to figure out runtime path vs. build path issue + { + urlPattern: /\/img\//, + handler: 'fastest' + }, + { + urlPattern: /https:\/\/thimble.mozilla.org\/img\//, + handler: 'fastest' + }, + + // Localization requires runtime caching of rewritten, locale-prefixed URLs + { + urlPattern: localesPattern, + handler: 'fastest' + }, + + // Various external deps we need + { + urlPattern: /^https:\/\/fonts\.googleapis\.com\/css/, + handler: 'fastest' + }, + { + urlPattern: /^https:\/\/fonts\.gstatic\.com\//, + handler: 'fastest' + }, + { + urlPattern: /^https:\/\/mozilla.github.io\/thimble-homepage-gallery\/activities.json/, + handler: 'fastest' + }, + { + urlPattern: /^https:\/\/pontoon.mozilla.org\/pontoon.js/, + handler: 'fastest' + } + ], + + navigateFallback: '/', + + ignoreUrlParametersMatching: [/./] + }; } + + function writeServiceWorker(config) { + swPrecache.write(Path.join(rootDir, 'thimble-sw.js'), config, function(err) { + if(err) { + grunt.fail.warn(err); + } + done(); + }); + } + }); grunt.registerTask("test", [ "jshint:server", "jshint:frontend", "lesslint" ]); diff --git a/package.json b/package.json index 62c338ad3..11cd0743a 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "q-io": "1.13.2", "request": "2.80.0", "serve-favicon": "^2.4.1", + "sw-precache": "^5.1.0", "tar-stream": "^1.5.2", "throng": "^4.0.0", "time-grunt": "^1.4.0", diff --git a/public/editor/scripts/main.js b/public/editor/scripts/main.js index d385d58eb..74572c379 100644 --- a/public/editor/scripts/main.js +++ b/public/editor/scripts/main.js @@ -1,3 +1,43 @@ + +/** + * Service Worker offline cache registration. The thimble-sw.js file + * is generated by Grunt as part of a dist/ build, and will not do anything + * in dev builds. + */ +if ('serviceWorker' in window.navigator) { + window.navigator.serviceWorker.register('/thimble-sw.js').then(function(reg) { + "use strict"; + + reg.onupdatefound = function() { + var installingWorker = reg.installing; + + installingWorker.onstatechange = function() { + switch (installingWorker.state) { + case 'installed': + if (window.navigator.serviceWorker.controller) { + // At this point, the old content will have been purged and the fresh content will + // have been added to the cache. + // It's the perfect time to display a "New content is available; please refresh." + // message in the page's interface. + console.log('[Thimble] New or updated content is available.'); + } else { + // At this point, everything has been precached. + // It's the perfect time to display a "Content is cached for offline use." message. + console.log('[Thimble] Content is now available offline!'); + } + break; + case 'redundant': + console.error('[Thimble] The installing service worker became redundant.'); + break; + } + }; + }; + }).catch(function(e) { + "use strict"; + console.error('[Bramble] Error during service worker registration:', e); + }); +} + // NOTE: if you change this, update Gruntfile's requirejs:dist task too require.config({ waitSeconds: 120, diff --git a/server/index.js b/server/index.js index 641e5b8cf..365cd5b4a 100644 --- a/server/index.js +++ b/server/index.js @@ -96,6 +96,7 @@ if(!!env.get("FORCE_SSL")) { */ Utils.getFileList(path.join(root, "public"), "!(*.js)") .forEach(file => server.use(express.static(file, maxCacheAge))); +server.use("/thimble-sw.js", express.static(path.join(root, "public/thimble-sw.js"), maxCacheAge)); server.use(express.static(cssAssets, maxCacheAge)); server.use(express.static(path.join(root, "public/resources"), maxCacheAge)); server.use("/node_modules", express.static(path.join(root, server.locals.node_path), maxCacheAge)); @@ -111,7 +112,7 @@ server.use("/resources/remix", express.static(path.join(root, "public/resources/ * L10N */ localize(server, Object.assign(env.get("L10N"), { - excludeLocaleInUrl: [ "/projects/remix-bar" ] + excludeLocaleInUrl: [ "/projects/remix-bar", "/thimble-sw.js" ] })); diff --git a/server/security.js b/server/security.js index f069f8513..346809a5b 100644 --- a/server/security.js +++ b/server/security.js @@ -9,6 +9,8 @@ let defaultCSPDirectives = { defaultSrc: [ "'self'" ], connectSrc: [ "'self'", + "https://fonts.googleapis.com", + "https://fonts.gstatic.com", "https://pontoon.mozilla.org", "https://mozilla.github.io/thimble-homepage-gallery/activities.json" ], From 58fa748f089b360c554a2e76bc1494e6058e5298 Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" Date: Wed, 12 Apr 2017 12:14:02 -0400 Subject: [PATCH 2/4] Add offline.js module to handle all online/offline events --- Gruntfile.js | 4 +- public/editor/scripts/editor/js/fc/offline.js | 79 +++++++++++++++++++ public/editor/scripts/main.js | 59 ++++---------- 3 files changed, 99 insertions(+), 43 deletions(-) create mode 100644 public/editor/scripts/editor/js/fc/offline.js diff --git a/Gruntfile.js b/Gruntfile.js index 493d09080..32b76453f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -171,9 +171,11 @@ module.exports = function( grunt ) { cacheId: 'thimble', logger: grunt.log.writeln, staticFileGlobs: [ + /* 'dist/editor/stylesheets/*.css', 'dist/resources/stylesheets/*.css', 'dist/homepage/stylesheets/*.css' + */ ], runtimeCaching: [ // TODO: we should be bundling all this vs. loading separate @@ -228,7 +230,7 @@ module.exports = function( grunt ) { } function writeServiceWorker(config) { - swPrecache.write(Path.join(rootDir, 'thimble-sw.js'), config, function(err) { + swPrecache.write(Path.join(/*rootDir*/'public', 'thimble-sw.js'), config, function(err) { if(err) { grunt.fail.warn(err); } diff --git a/public/editor/scripts/editor/js/fc/offline.js b/public/editor/scripts/editor/js/fc/offline.js new file mode 100644 index 000000000..1789c7abf --- /dev/null +++ b/public/editor/scripts/editor/js/fc/offline.js @@ -0,0 +1,79 @@ +/** + * Consolidate all online/offline and Service Worker events from both Thimble and Bramble. + * The events we trigger include: + * + * - `updatesAvailable`: one of Thimble and/or Brackets has updates in the cache, user should reload + * - `offlineReady`: one of Thimble and/or Brackets has cached its content + * - `online`: the browser has re-established a network connection + * - `offline`: the browser has lost its network connection + * + * You can also use `Offline.isOnline()` to check the online status. + */ +define(function(require) { + + var EventEmitter = require("EventEmitter"); + var Offline = new EventEmitter(); + + /** + * Service Worker offline cache registration. The thimble-sw.js file + * is generated by Grunt as part of a dist/ build, and will not do anything + * in dev builds. + */ + function initServiceWorker() { + if (!('serviceWorker' in window.navigator)) { + return; + } + + window.navigator.serviceWorker.register('/thimble-sw.js').then(function(reg) { + reg.onupdatefound = function() { + var installingWorker = reg.installing; + + installingWorker.onstatechange = function() { + switch (installingWorker.state) { + case 'installed': + if (window.navigator.serviceWorker.controller) { + // Cache has been updated + Offline.trigger("updatesAvailable"); + } else { + // Cache has been filled + Offline.trigger("offlineReady"); + } + break; + case 'redundant': + console.error('[Thimble] The installing service worker became redundant.'); + break; + } + }; + }; + }).catch(function(e) { + "use strict"; + console.error('[Bramble] Error during service worker registration:', e); + }); + } + + Offline.init = function(Bramble) { + initServiceWorker(); + + // Listen for sw events from Bramble, and consolidate with our own. + Bramble.on("updatesAvailable", function() { + Offline.trigger("updatesAvailable"); + }) + Bramble.on("offlineReady", function() { + Offline.trigger("offlineReady"); + }) + + // Listen for online/offline events from the browser + window.addEventListener("offline", function() { + Offline.trigger("offline"); + }, false); + window.addEventListener("online", function() { + Offline.trigger("online"); + }); + } + + Offline.isOnline = function() { + return navigator.onLine; + } + + return Offline; +}); diff --git a/public/editor/scripts/main.js b/public/editor/scripts/main.js index 74572c379..7a2d12c32 100644 --- a/public/editor/scripts/main.js +++ b/public/editor/scripts/main.js @@ -1,43 +1,3 @@ - -/** - * Service Worker offline cache registration. The thimble-sw.js file - * is generated by Grunt as part of a dist/ build, and will not do anything - * in dev builds. - */ -if ('serviceWorker' in window.navigator) { - window.navigator.serviceWorker.register('/thimble-sw.js').then(function(reg) { - "use strict"; - - reg.onupdatefound = function() { - var installingWorker = reg.installing; - - installingWorker.onstatechange = function() { - switch (installingWorker.state) { - case 'installed': - if (window.navigator.serviceWorker.controller) { - // At this point, the old content will have been purged and the fresh content will - // have been added to the cache. - // It's the perfect time to display a "New content is available; please refresh." - // message in the page's interface. - console.log('[Thimble] New or updated content is available.'); - } else { - // At this point, everything has been precached. - // It's the perfect time to display a "Content is cached for offline use." message. - console.log('[Thimble] Content is now available offline!'); - } - break; - case 'redundant': - console.error('[Thimble] The installing service worker became redundant.'); - break; - } - }; - }; - }).catch(function(e) { - "use strict"; - console.error('[Bramble] Error during service worker registration:', e); - }); -} - // NOTE: if you change this, update Gruntfile's requirejs:dist task too require.config({ waitSeconds: 120, @@ -66,7 +26,7 @@ require.config({ } }); -require(["jquery", "bowser"], function($, bowser) { +require(["jquery", "bowser", "fc/offline"], function($, bowser, Offline) { // Warn users of unsupported browsers that they can try something newer, // specifically anything before IE 11 or Safari 8. if((bowser.msie && bowser.version < 11) || (bowser.safari && bowser.version < 8)) { @@ -92,7 +52,22 @@ require(["jquery", "bowser"], function($, bowser) { Bramble.once("error", onError); - function init(BrambleEditor, Project, SSOOverride, ProjectRenameUtility) { + // Initialize offline/online handling + Offline.init(Bramble); + Offline.on("online", function() { + console.log("Offline: online event") + }); + Offline.on("offline", function() { + console.log("Offline: offline event") + }); + Offline.on('offlineReady', function() { + console.log("Offline: offlineReady event") + }); + Offline.on('updatesAvailable', function() { + console.log("Offline: upddatesAvailable event") + }); + + function init(BrambleEditor, Project, SSOOverride, ProjectRenameUtility, Offline) { var thimbleScript = document.getElementById("thimble-script"); var appUrl = thimbleScript.getAttribute("data-app-url"); var projectDetails = thimbleScript.getAttribute("data-project-details"); From 7437a1a30f1f970e721e7e626acd4fa9021a76b8 Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" Date: Wed, 12 Apr 2017 13:11:29 -0400 Subject: [PATCH 3/4] Fix travis issues, remove console logging and offlineReady event for Thimble --- public/editor/scripts/editor/js/fc/offline.js | 12 ++++-------- public/editor/scripts/main.js | 14 +------------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/public/editor/scripts/editor/js/fc/offline.js b/public/editor/scripts/editor/js/fc/offline.js index 1789c7abf..8e340dea3 100644 --- a/public/editor/scripts/editor/js/fc/offline.js +++ b/public/editor/scripts/editor/js/fc/offline.js @@ -3,7 +3,6 @@ * The events we trigger include: * * - `updatesAvailable`: one of Thimble and/or Brackets has updates in the cache, user should reload - * - `offlineReady`: one of Thimble and/or Brackets has cached its content * - `online`: the browser has re-established a network connection * - `offline`: the browser has lost its network connection * @@ -34,9 +33,6 @@ define(function(require) { if (window.navigator.serviceWorker.controller) { // Cache has been updated Offline.trigger("updatesAvailable"); - } else { - // Cache has been filled - Offline.trigger("offlineReady"); } break; case 'redundant': @@ -57,10 +53,10 @@ define(function(require) { // Listen for sw events from Bramble, and consolidate with our own. Bramble.on("updatesAvailable", function() { Offline.trigger("updatesAvailable"); - }) + }); Bramble.on("offlineReady", function() { Offline.trigger("offlineReady"); - }) + }); // Listen for online/offline events from the browser window.addEventListener("offline", function() { @@ -69,11 +65,11 @@ define(function(require) { window.addEventListener("online", function() { Offline.trigger("online"); }); - } + }; Offline.isOnline = function() { return navigator.onLine; - } + }; return Offline; }); diff --git a/public/editor/scripts/main.js b/public/editor/scripts/main.js index 7a2d12c32..7715f251e 100644 --- a/public/editor/scripts/main.js +++ b/public/editor/scripts/main.js @@ -54,20 +54,8 @@ require(["jquery", "bowser", "fc/offline"], function($, bowser, Offline) { // Initialize offline/online handling Offline.init(Bramble); - Offline.on("online", function() { - console.log("Offline: online event") - }); - Offline.on("offline", function() { - console.log("Offline: offline event") - }); - Offline.on('offlineReady', function() { - console.log("Offline: offlineReady event") - }); - Offline.on('updatesAvailable', function() { - console.log("Offline: upddatesAvailable event") - }); - function init(BrambleEditor, Project, SSOOverride, ProjectRenameUtility, Offline) { + function init(BrambleEditor, Project, SSOOverride, ProjectRenameUtility) { var thimbleScript = document.getElementById("thimble-script"); var appUrl = thimbleScript.getAttribute("data-app-url"); var projectDetails = thimbleScript.getAttribute("data-project-details"); From 3a804dd650966293274b49f0238f52f029bcc8b1 Mon Sep 17 00:00:00 2001 From: "David Humphrey (:humph) david.humphrey@senecacollege.ca" Date: Wed, 12 Apr 2017 16:40:49 -0400 Subject: [PATCH 4/4] A few fixes --- Gruntfile.js | 9 +++------ server/index.js | 3 ++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 32b76453f..5d3ffc5c2 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -146,7 +146,6 @@ module.exports = function( grunt ) { grunt.registerMultiTask('swPrecache', function() { var done = this.async(); var rootDir = this.data.rootDir; - rootDir = "get rid of this later"; // We need the full list of locales so we can create runtime caching rules for each fs.readdir('locales', function(err, locales) { @@ -171,7 +170,7 @@ module.exports = function( grunt ) { cacheId: 'thimble', logger: grunt.log.writeln, staticFileGlobs: [ - /* + /* TODO: we need to not localize these asset dirs so we can statically cache 'dist/editor/stylesheets/*.css', 'dist/resources/stylesheets/*.css', 'dist/homepage/stylesheets/*.css' @@ -223,14 +222,12 @@ module.exports = function( grunt ) { } ], - navigateFallback: '/', - ignoreUrlParametersMatching: [/./] }; } function writeServiceWorker(config) { - swPrecache.write(Path.join(/*rootDir*/'public', 'thimble-sw.js'), config, function(err) { + swPrecache.write(Path.join(rootDir, 'thimble-sw.js'), config, function(err) { if(err) { grunt.fail.warn(err); } @@ -241,6 +238,6 @@ module.exports = function( grunt ) { }); grunt.registerTask("test", [ "jshint:server", "jshint:frontend", "lesslint" ]); - grunt.registerTask("build", [ "test", "requirejs:dist" ]); + grunt.registerTask("build", [ "test", "requirejs:dist", "swPrecache" ]); grunt.registerTask("default", [ "test" ]); }; diff --git a/server/index.js b/server/index.js index 365cd5b4a..b06d58438 100644 --- a/server/index.js +++ b/server/index.js @@ -96,7 +96,8 @@ if(!!env.get("FORCE_SSL")) { */ Utils.getFileList(path.join(root, "public"), "!(*.js)") .forEach(file => server.use(express.static(file, maxCacheAge))); -server.use("/thimble-sw.js", express.static(path.join(root, "public/thimble-sw.js"), maxCacheAge)); +// Don't cache sw script +server.use("/thimble-sw.js", express.static(path.join(root, "public/thimble-sw.js"), { maxAge: 0 })); server.use(express.static(cssAssets, maxCacheAge)); server.use(express.static(path.join(root, "public/resources"), maxCacheAge)); server.use("/node_modules", express.static(path.join(root, server.locals.node_path), maxCacheAge));