diff --git a/Gruntfile.js b/Gruntfile.js index 907e709c3..5d3ffc5c2 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,10 +135,109 @@ module.exports = function( grunt ) { ] } } + }, + swPrecache: { + dist: { + rootDir: 'dist' + } } }); + grunt.registerMultiTask('swPrecache', function() { + var done = this.async(); + var rootDir = this.data.rootDir; + + // 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: [ + /* 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' + */ + ], + 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' + } + ], + + 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" ]); - grunt.registerTask("build", [ "test", "requirejs:dist" ]); + grunt.registerTask("build", [ "test", "requirejs:dist", "swPrecache" ]); grunt.registerTask("default", [ "test" ]); }; 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/editor/js/fc/offline.js b/public/editor/scripts/editor/js/fc/offline.js new file mode 100644 index 000000000..8e340dea3 --- /dev/null +++ b/public/editor/scripts/editor/js/fc/offline.js @@ -0,0 +1,75 @@ +/** + * 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 + * - `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"); + } + 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 d385d58eb..7715f251e 100644 --- a/public/editor/scripts/main.js +++ b/public/editor/scripts/main.js @@ -26,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)) { @@ -52,6 +52,9 @@ require(["jquery", "bowser"], function($, bowser) { Bramble.once("error", onError); + // Initialize offline/online handling + Offline.init(Bramble); + function init(BrambleEditor, Project, SSOOverride, ProjectRenameUtility) { var thimbleScript = document.getElementById("thimble-script"); var appUrl = thimbleScript.getAttribute("data-app-url"); diff --git a/server/index.js b/server/index.js index 641e5b8cf..b06d58438 100644 --- a/server/index.js +++ b/server/index.js @@ -96,6 +96,8 @@ if(!!env.get("FORCE_SSL")) { */ Utils.getFileList(path.join(root, "public"), "!(*.js)") .forEach(file => server.use(express.static(file, 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)); @@ -111,7 +113,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" ],