Comments (19)
Looking at the generate-sw
flow the scenarios of first use, repeat use and CI usage could be performs like this:
from workbox.
This is shaping up really well. Great work, @gauntface.
- Which files would you like to cache prompt: are all of these unselected by default? Trying to figure out if the default behavior is to cache all file types found in
dist
- Offline we discussed having a skip/y/force mode to skip the prompts. Is something like that still of interest?
- There are almost two different config files users have to think about here - the file manifest and the sw-cli-config. I understand how the file manifest works, but can't recall why we decided on two manifest files rather than having them both live in one shared .json file. Thoughts? (probably missing something obvious)
from workbox.
- I was going to make it cache all by default, reasoning being that I imagine most developers will want all files in the directory to be made available via their web server and hence be available by SW. I think sw-precache has the same behavior (although sw-precache will not cache files above a certain size, which this will do as well).
- The last example is what I was thinking for the skip and / or force. Thinking about it, these should probably just be one flag. Original thinking was that -non-interactive meant that defaults were chosen and --force would tell the CLI to force the overwrite of the SW and File manifest.
- The 2 commands 'generate-sw' and 'build-manifest' are very similar. The only difference is that the generate-sw command would build both the manifest and a service worker. So building the manifest can use the same config file as generating the service worker. I'm open to any ideas to improve this (because I agree it's not completely obvious as to why there are two commands). Originally I was leaning towards an
init
command that would build the config file and then abuild
command that would build the required fields and the developer would have to add flags for generating sw / manifest.
from workbox.
Note to self: Add tool to notify of CLI update.
from workbox.
@gauntface For Yeoman we ended up throwing together update-notifier, which looks a little like this:
It just checks your package.json for a version number and then checks for any available updates. You can call notify()
at any point in your CLI lifecycle to prompt users about new versions being available.
from workbox.
Yeah that's what I intended to add in.
from workbox.
Drive-by feedback:
- I liked that sw-cli detected the subdirectory I wanted to cache correctly
- It correctly detected the extensions present (at least I think it did) and prompted me to choose what to cache, defaulting to all (my expectation)
- The name of the service worker file defaulted to
sw.js
. I wonder ifservice-worker.js
would be more beginner friendly for when they get to debugging and are looking around for where their SW is. - Last CLI question was 'Would you like to save these settings to a config file?`. With my user hat on, I was confused why another config file (we had a manifest earlier in the wizard) was needed. I'm likely to just hit 'yes', but wonder if there's more we can do to make the distinction clearer. Or if we need to.
- Loved that the output included a summary of exactly what had been done. Nice work.
from workbox.
Just checked with @gauntface. Atm, the CLI just generates a precache manifest but not the service worker for you. Will test again once it's able to handle that part.
from workbox.
Use cases for CLI
- Developer uses CLI to generate SW + manifest (i.e. all in one solution).
- Developer uses CLI to generate file manifest (i.e. developer wants to generate just a list of files and hashes).
- Developer takes generated SW and alters it and uses it going forward (do we support them or leave them to fend for themselves?).
Generate SW + Manifest
Goals of this in my head are to:
- produce an easy to understand service worker.
- Make it clear how our libraries are used.
- Make it possible to take this as a base and edit going forward.
Option 1
This follows what most of the team are doing for their service workers.
The downsides of this approach are:
- I can see scenarios where any attempt to update the manifest from the CLI will be risky.
- Developers are likely to manually change the manifest without any real consideration for the revision information.
importScripts('/sw-lib.12345.min.js');
const fileManifest = [
{
url: '/',
revision: '1234'
},
{
....
}
];
self.goog.swlib.cacheRevisionedAssets(fileManifest);
Option 2
Alternative, but goes against what everyone does naturally is to have the file manifest. Downsides:
- Still has a risk if the CLI updates the manifest file only (i.e. doesn't rebuild the SW), but the tool would have a config with the last known manifest name so should be easier to replace if the user altered the service worker.
- Not naturally how others have built their service workers.
importScripts(
'/precache-manifest.12345.js',
'/sw-lib.12345.min.js'
);
self.goog.swlib.cacheRevisionedAssets(self.__file_manifest);
Generate File Manifest Only
Goals of this in my head are to:
- Developer has a service worker but want to take advantage of the file re-visioning logic in the CLI / Module.
- Make it possible to update the file manifest and update service worker with this new manifest (this would require permission from user to alter their service worker).
For developers who want to inline the manifest they should be able to get the file manifest from the cli tool as a node module to use in a build step OR use the CLI tool to generate the JS file and read into their SW file via import scripts or bundling somehow.
If the file manifest in the generated service worker is an import we can update the import.
If we inline the file manifest in the generated service worker we should leave it to the developer to decide how to take the manifest and put it in their service worker (i.e. no help), but we can provide guidance on how to handle this in docs.
Developer Customises Generated SW
This scenario is just a mash up of the two scenarios above. They generate a service worker, start to use it and add functionality (Push or bg sync or custom routes) and the question becomes, how do they update the sw-lib and / or the file manifest.
- If the manifest is inlined, there isn't much we can do safely. Importing has some sane approaches.
- Updating the sw-lib is an open question. Ideally the developer would move to the node_module. Failing that we can detect if the file hash is different (Maybe the hash can be the version number) and flag it to the developer to update if we notice it's out of date.
from workbox.
Another option is to leave the files names as is and adding comments that are the hashes of the files imported. Updating the service worker and easier to manage files. Risk is it's not clear what the hashes are for.
from workbox.
Here's what I had envisioned:
The initial bootstrapping would ask questions about which files to precache, etc. and generate the manifest, along with a saved configuration that could be used to automatically regenerate the manifest each time your site it rebuilt. I believe that part is already implemented in sw-cli
.
The initial bootstrapping can optionally create a sw.js
that would be a starting point for developers to use. The file could consist simply of:
import manifest from 'path/to/manifest';
import SWLib from 'path/to/sw-lib';
// I forget the actual name of the method exposed to manage precaching,
// but you get the idea.
SWLib.managePrecache(manifest);
// As long as the preceding three lines are somewhere in sw.js, the developer
// can do whatever else they want, i.e. use SWLib to set up routes, use push
// messaging, whatever.
The requirement is that sw.js
, whether it was bootstrapped by sw-cli
or whether someone wrote it 100% by hand, would need to be run through an ES2015 module bundler like Rollup after each build/manifest update cycle, prior to deployment. The module bundler would ensure that the latest versions of all imported modules are inlined into the final sw.js
that gets deployed.
I think it would make sense if sw-cli
could support that bundling at least as an option, but if a developer prefers, they could bundle it themselves.
Once there's widely deployed support for importScripts
triggering the SW update, then using importScripts('path/to/manifest', 'path/to/sw-lib')
as an alternative to ES2015 imports would be a viable alternative. That would mean that the bundling step wasn't necessary, but the additional deployment of those two standalone JS files would be required. We're not at that point yet, though, so requiring ES2015 imports seems reasonable to me.
This approach seems to preserve the readability and flexibility of the SW during development, prior to deployment. There's a requirement that you list the ES2015 imports somewhere in the file, but that same requirement would apply with the importScripts()
syntax.
from workbox.
I'm not a fan of the rollup bundling because it assumes the developer wants to use ES2015 imports. Some developers use browserify for their JS and in this scenario rollup just adds a complication.
If a developer wishes to write their service worker like this, I assume they'd be more than happy to just adapt the working JS service worker (i.e. a version using importScript() ) to their preference of build process.
Finally, I don't think a developer would ever see the version you'd included, if sw-cli uses rollup, the output is:
/*
Copyright 2016 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):(e.goog=e.goog||{},e.goog.swlib=t())})(this,function(){'use strict';function e(be,Ee){for(var ke,Ne=[],xe=0,qe=0,Ce='',Ie=Ee&&Ee.delimiter||'/';null!=(ke=S.exec(be));){var Te=ke[0],we=ke[1],Se=ke.index;if(Ce+=be.slice(qe,Se),qe=Se+Te.length,we){Ce+=we[1];continue}var De=be[qe],Pe=ke[2],je=ke[3],Ae=ke[4],Me=ke[5],Oe=ke[6],We=ke[7];Ce&&(Ne.push(Ce),Ce='');var Re=ke[2]||Ie,Ue=Ae||Me;Ne.push({name:je||xe++,prefix:Pe||'',delimiter:Re,optional:'?'===Oe||'*'===Oe,repeat:'+'===Oe||'*'===Oe,partial:null!=Pe&&null!=De&&De!==Pe,asterisk:!!We,pattern:Ue?s(Ue):We?'.*':'[^'+n(Re)+']+?'})}return qe<be.length&&(Ce+=be.substr(qe)),Ce&&Ne.push(Ce),Ne}function t(be){return encodeURI(be).replace(/[\/?#]/g,function(Ee){return'%'+Ee.charCodeAt(0).toString(16).toUpperCase()})}function r(be){return encodeURI(be).replace(/[?#]/g,function(Ee){return'%'+Ee.charCodeAt(0).toString(16).toUpperCase()})}function a(be){var Ee=Array(be.length);for(var Ne=0;Ne<be.length;Ne++)'object'==typeof be[Ne]&&(Ee[Ne]=new RegExp('^(?:'+be[Ne].pattern+')$'));return function(xe,qe){var Ce='',Ie=xe||{},ke=(qe||{}).pretty?t:encodeURIComponent;for(var Te=0;Te<be.length;Te++){var we=be[Te];if('string'==typeof we){Ce+=we;continue}var De,Se=Ie[we.name];if(null==Se)if(we.optional){we.partial&&(Ce+=we.prefix);continue}else throw new TypeError('Expected "'+we.name+'" to be defined');if(T(Se)){if(!we.repeat)throw new TypeError('Expected "'+we.name+'" to not repeat, but received `'+JSON.stringify(Se)+'`');if(0===Se.length)if(we.optional)continue;else throw new TypeError('Expected "'+we.name+'" to not be empty');for(var Pe=0;Pe<Se.length;Pe++){if(De=ke(Se[Pe]),!Ee[Te].test(De))throw new TypeError('Expected all "'+we.name+'" to match "'+we.pattern+'", but received `'+JSON.stringify(De)+'`');Ce+=(0===Pe?we.prefix:we.delimiter)+De}continue}if(De=we.asterisk?r(Se):ke(Se),!Ee[Te].test(De))throw new TypeError('Expected "'+we.name+'" to match "'+we.pattern+'", but received "'+De+'"');Ce+=we.prefix+De}return Ce}}function n(be){return be.replace(/([.+*?=^!:${}()[\]|\/\\])/g,'\\$1')}function s(be){return be.replace(/([=!:$\/()])/g,'\\$1')}function o(be,Ee){return be.keys=Ee,be}function d(be){return be.sensitive?'':'i'}function l(be,Ee){var Ne=be.source.match(/\((?!\?)/g);if(Ne)for(var xe=0;xe<Ne.length;xe++)Ee.push({name:xe,prefix:null,delimiter:null,optional:!1,repeat:!1,partial:!1,asterisk:!1,pattern:null});return o(be,Ee)}function h(be,Ee,Ne){var xe=[];for(var qe=0;qe<be.length;qe++)xe.push(f(be[qe],Ee,Ne).source);var Ce=new RegExp('(?:'+xe.join('|')+')',d(Ne));return o(Ce,Ee)}function u(be,Ee,Ne){return g(e(be,Ne),Ee,Ne)}function g(be,Ee,Ne){T(Ee)||(Ne=Ee||Ne,Ee=[]),Ne=Ne||{};var xe=Ne.strict,qe=!1!==Ne.end,Ce='';for(var Ie=0;Ie<be.length;Ie++){var ke=be[Ie];if('string'==typeof ke)Ce+=n(ke);else{var Te=n(ke.prefix),we='(?:'+ke.pattern+')';Ee.push(ke),ke.repeat&&(we+='(?:'+Te+we+')*'),we=ke.optional?ke.partial?Te+'('+we+')?':'(?:'+Te+'('+we+'))?':Te+'('+we+')',Ce+=we}}var Se=n(Ne.delimiter||'/'),De=Ce.slice(-Se.length)===Se;return xe||(Ce=(De?Ce.slice(0,-Se.length):Ce)+'(?:'+Se+'(?=$))?'),Ce+=qe?'$':xe&&De?'':'(?='+Se+'|$)',o(new RegExp('^'+Ce,d(Ne)),Ee)}function f(be,Ee,Ne){return T(Ee)||(Ne=Ee||Ne,Ee=[]),Ne=Ne||{},be instanceof RegExp?l(be,Ee):T(be)?h(be,Ee,Ne):u(be,Ee,Ne)}function y({channel:be,cacheName:Ee,url:Ne,source:xe}){q.isInstance({channel:be},BroadcastChannel),q.isType({cacheName:Ee},'string'),q.isType({source:xe},'string'),q.isType({url:Ne},'string'),be.postMessage({type:'CACHE_UPDATED',meta:xe,payload:{cacheName:Ee,updatedUrl:Ne}})}function _({first:be,second:Ee,headersToCheck:Ne}){return q.isInstance({first:be},Response),q.isInstance({second:Ee},Response),q.isInstance({headersToCheck:Ne},Array),Ne.every(xe=>{return be.headers.has(xe)===Ee.headers.has(xe)&&be.headers.get(xe)===Ee.headers.get(xe)})}class v{constructor(be){this._errors=be}createError(be,Ee){if(!(be in this._errors))throw new Error(`Unable to generate error '${be}'.`);let Ne=this._errors[be].replace(/\s+/g,' '),xe=null;Ee&&(Ne+=` [${Ee.message}]`,xe=Ee.stack);const qe=new Error;return qe.name=be,qe.message=Ne,qe.stack=xe,qe}}const b={'not-in-sw':'sw-lib must be loaded in your service worker file.','unsupported-route-type':'Routes must be either a express style route string, a Regex to capture request URLs or a Route instance.','empty-express-string':'The Express style route string must have some characters, an empty string is invalid.','bad-revisioned-cache-list':`The 'cacheRevisionedAssets()' method expects`+`an array of revisioned urls like so: ['/example/hello.1234.txt', `+`{path: 'hello.txt', revision: '1234'}]`};var E=new v(b);const N={'express-route-requires-absolute-path':`When using ExpressRoute, you must
provide a path that starts with a '/' character. You can only match
same-origin requests. For more flexibility, use RegExpRoute.`};var x=new v(N),q={atLeastOne:function(Ee){const Ne=Object.keys(Ee);if(!Ne.some(xe=>void 0!==Ee[xe]))throw Error('Please set at least one of the following parameters: '+Ne.map(xe=>`'${xe}'`).join(', '))},hasMethod:function(Ee,Ne){const xe=Object.keys(Ee).pop(),qe=typeof Ee[xe][Ne];if('function'!=qe)throw Error(`The '${xe}' parameter must be an object that exposes `+`a '${Ne}' method.`)},isInstance:function(Ee,Ne){const xe=Object.keys(Ee).pop();if(!(Ee[xe]instanceof Ne))throw Error(`The '${xe}' parameter must be an instance of `+`'${Ne.name}'`)},isOneOf:function(Ee,Ne){const xe=Object.keys(Ee).pop();if(!Ne.includes(Ee[xe]))throw Error(`The '${xe}' parameter must be set to one of the `+`following: ${Ne}`)},isType:function(Ee,Ne){const xe=Object.keys(Ee).pop(),qe=typeof Ee[xe];if(qe!==Ne)throw Error(`The '${xe}' parameter has the wrong type. `+`(Expected: ${Ne}, actual: ${qe})`)},isSWEnv:function(){return'ServiceWorkerGlobalScope'in self&&self instanceof ServiceWorkerGlobalScope},isValue:function(Ee,Ne){const xe=Object.keys(Ee).pop(),qe=Ee[xe];if(qe!==Ne)throw Error(`The '${xe}' parameter has the wrong value. `+`(Expected: ${Ne}, actual: ${qe})`)}};const C=['DELETE','GET','HEAD','POST','PUT'];class I{constructor({match:be,handler:Ee,method:Ne}={}){q.isType({match:be},'function'),q.hasMethod({handler:Ee},'handle'),this.match=be,this.handler=Ee,Ne?(q.isOneOf({method:Ne},C),this.method=Ne):this.method='GET'}}var k=Array.isArray||function(be){return'[object Array]'==Object.prototype.toString.call(be)},T=k,w=f,S=new RegExp(['(\\\\.)','([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))'].join('|'),'g');w.parse=e,w.compile=function(Ee,Ne){return a(e(Ee,Ne))},w.tokensToFunction=a,w.tokensToRegExp=g;class D extends I{constructor({path:be,handler:Ee,method:Ne}){if('/'!==be.substring(0,1))throw x.createError('express-route-requires-absolute-path');let xe=[];const qe=w(be,xe);super({match:({url:Ce})=>{if(Ce.origin!==location.origin)return null;const Ie=Ce.pathname.match(qe);if(!Ie)return null;const ke={};return xe.forEach((Te,we)=>{ke[Te.name]=Ie[we+1]}),ke},handler:Ee,method:Ne})}}class P extends I{constructor({regExp:be,handler:Ee,method:Ne}){q.isInstance({regExp:be},RegExp),super({match:({url:xe})=>{const qe=xe.href.match(be);return qe?qe.slice(1):null},handler:Ee,method:Ne})}}class A{setDefaultHandler({handler:be}={}){q.hasMethod({handler:be},'handle'),this.defaultHandler=be}setCatchHandler({handler:be}={}){q.hasMethod({handler:be},'handle'),this.catchHandler=be}registerRoutes({routes:be}={}){q.isInstance({routes:be},Array),self.addEventListener('fetch',Ee=>{const Ne=new URL(Ee.request.url);if(Ne.protocol.startsWith('http')){let xe;for(let qe of be||[])if(qe.method===Ee.request.method){const Ce=qe.match({url:Ne,event:Ee});if(Ce){let Ie=Ce;Array.isArray(Ie)&&0===Ie.length?Ie=void 0:Ie.constructor===Object&&0===Object.keys(Ie).length&&(Ie=void 0),xe=qe.handler.handle({url:Ne,event:Ee,params:Ie});break}}!xe&&this.defaultHandler&&(xe=this.defaultHandler.handle({url:Ne,event:Ee})),xe&&this.catchHandler&&(xe=xe.catch(qe=>{return this.catchHandler.handle({url:Ne,event:Ee,error:qe})})),xe&&Ee.respondWith(xe)}})}registerRoute({route:be}={}){q.isInstance({route:be},I),this.registerRoutes({routes:[be]})}}class M{constructor(){this._router=new A}registerRoute(be,Ee){if('function'==typeof Ee&&(Ee={handle:Ee}),'string'==typeof be){if(0===be.length)throw E.createError('empty-express-string');this._router.registerRoute({route:new D({path:be,handler:Ee})})}else if(be instanceof RegExp)this._router.registerRoute({route:new P({regExp:be,handler:Ee})});else if(be instanceof I)this._router.registerRoute({route:be});else throw E.createError('unsupported-route-type')}}const O={'not-in-sw':'sw-precaching must be loaded in your service worker file.','invalid-revisioned-entry':`File manifest entries must be either a `+`string with revision info in the url or an object with a 'url' and `+`'revision' parameters.`,'invalid-unrevisioned-entry':``,'bad-cache-bust':`The cache bust parameter must be a boolean.`,'duplicate-entry-diff-revisions':`An attempt was made to cache the same `+`url twice with each having different revisions. This is not supported.`,'request-not-cached':`A request failed the criteria to be cached. By `+`default, only responses with 'response.ok = true' are cached.`,'should-override':'Method should be overridden by the extending class.'};var W=new v(O),R=function(be){return function(){var Ee=be.apply(this,arguments);return new Promise(function(Ne,xe){function qe(Ce,Ie){try{var ke=Ee[Ce](Ie),Te=ke.value}catch(we){return void xe(we)}return ke.done?void Ne(Te):Promise.resolve(Te).then(function(we){qe('next',we)},function(we){qe('throw',we)})}return qe('next')})}};class U{constructor(be){this._entriesToCache=new Map,this._cacheName=be}cache(be){be.forEach(Ee=>{this._addEntryToInstallList(this._parseEntry(Ee))})}_addEntryToInstallList(be){const Ee=be.entryID,Ne=this._entriesToCache.get(be.entryID);return Ne?void this._onDuplicateInstallEntryFound(be,Ne):void this._entriesToCache.set(Ee,be)}_performInstallStep(){var be=this;return R(function*(){if(0!==be._entriesToCache.size){const Ee=[];return be._entriesToCache.forEach(function(Ne){Ee.push(be._cacheEntry(Ne))}),Promise.all(Ee)}})()}_cacheEntry(be){var Ee=this;return R(function*(){const Ne=yield Ee._isAlreadyCached(be);if(!Ne){let xe=yield fetch(be.getNetworkRequest(),{credentials:'same-origin',redirect:'follow'});if(xe.ok){const qe=yield Ee._getCache();return yield qe.put(be.request,xe),Ee._onEntryCached(be)}throw W.createError('request-not-cached',{message:`Failed to get a cacheable response for `+`'${be.request.url}'`})}})()}_cleanUpOldEntries(){var be=this;return R(function*(){if(yield caches.has(be._cacheName)){const Ee=[];be._entriesToCache.forEach(function(Ce){Ee.push(Ce.request.url)});const Ne=yield be._getCache(),xe=yield Ne.keys(),qe=xe.filter(function(Ce){return!Ee.includes(Ce.url)});return Promise.all(qe.map(function(Ce){return Ne.delete(Ce)}))}})()}_getCache(){var be=this;return R(function*(){return be._cache||(be._cache=yield caches.open(be._cacheName)),be._cache})()}_parseEntry(be){throw W.createError('should-override')}_onDuplicateEntryFound(be,Ee){throw W.createError('should-override')}_isAlreadyCached(be){throw W.createError('should-override')}_onEntryCached(be){throw W.createError('should-override')}}var B=function(Ee,Ne){return Ne={exports:{}},Ee(Ne,Ne.exports),Ne.exports}(function(be){'use strict';(function(){function Ee(Oe){return Array.prototype.slice.call(Oe)}function Ne(Oe){return new Promise(function(We,Re){Oe.onsuccess=function(){We(Oe.result)},Oe.onerror=function(){Re(Oe.error)}})}function xe(Oe,We,Re){var Ue,Be=new Promise(function(Ke,Fe){Ue=Oe[We].apply(Oe,Re),Ne(Ue).then(Ke,Fe)});return Be.request=Ue,Be}function qe(Oe,We,Re){var Ue=xe(Oe,We,Re);return Ue.then(function(Be){if(Be)return new Se(Be,Ue.request)})}function Ce(Oe,We,Re){Re.forEach(function(Ue){Object.defineProperty(Oe.prototype,Ue,{get:function(){return this[We][Ue]},set:function(Be){this[We][Ue]=Be}})})}function Ie(Oe,We,Re,Ue){Ue.forEach(function(Be){Be in Re.prototype&&(Oe.prototype[Be]=function(){return xe(this[We],Be,arguments)})})}function ke(Oe,We,Re,Ue){Ue.forEach(function(Be){Be in Re.prototype&&(Oe.prototype[Be]=function(){return this[We][Be].apply(this[We],arguments)})})}function Te(Oe,We,Re,Ue){Ue.forEach(function(Be){Be in Re.prototype&&(Oe.prototype[Be]=function(){return qe(this[We],Be,arguments)})})}function we(Oe){this._index=Oe}function Se(Oe,We){this._cursor=Oe,this._request=We}function De(Oe){this._store=Oe}function Pe(Oe){this._tx=Oe,this.complete=new Promise(function(We,Re){Oe.oncomplete=function(){We()},Oe.onerror=function(){Re(Oe.error)},Oe.onabort=function(){Re(Oe.error)}})}function je(Oe,We,Re){this._db=Oe,this.oldVersion=We,this.transaction=new Pe(Re)}function Ae(Oe){this._db=Oe}Ce(we,'_index',['name','keyPath','multiEntry','unique']),Ie(we,'_index',IDBIndex,['get','getKey','getAll','getAllKeys','count']),Te(we,'_index',IDBIndex,['openCursor','openKeyCursor']),Ce(Se,'_cursor',['direction','key','primaryKey','value']),Ie(Se,'_cursor',IDBCursor,['update','delete']),['advance','continue','continuePrimaryKey'].forEach(function(Oe){Oe in IDBCursor.prototype&&(Se.prototype[Oe]=function(){var We=this,Re=arguments;return Promise.resolve().then(function(){return We._cursor[Oe].apply(We._cursor,Re),Ne(We._request).then(function(Ue){if(Ue)return new Se(Ue,We._request)})})})}),De.prototype.createIndex=function(){return new we(this._store.createIndex.apply(this._store,arguments))},De.prototype.index=function(){return new we(this._store.index.apply(this._store,arguments))},Ce(De,'_store',['name','keyPath','indexNames','autoIncrement']),Ie(De,'_store',IDBObjectStore,['put','add','delete','clear','get','getAll','getKey','getAllKeys','count']),Te(De,'_store',IDBObjectStore,['openCursor','openKeyCursor']),ke(De,'_store',IDBObjectStore,['deleteIndex']),Pe.prototype.objectStore=function(){return new De(this._tx.objectStore.apply(this._tx,arguments))},Ce(Pe,'_tx',['objectStoreNames','mode']),ke(Pe,'_tx',IDBTransaction,['abort']),je.prototype.createObjectStore=function(){return new De(this._db.createObjectStore.apply(this._db,arguments))},Ce(je,'_db',['name','version','objectStoreNames']),ke(je,'_db',IDBDatabase,['deleteObjectStore','close']),Ae.prototype.transaction=function(){return new Pe(this._db.transaction.apply(this._db,arguments))},Ce(Ae,'_db',['name','version','objectStoreNames']),ke(Ae,'_db',IDBDatabase,['close']),['openCursor','openKeyCursor'].forEach(function(Oe){[De,we].forEach(function(We){We.prototype[Oe.replace('open','iterate')]=function(){var Re=Ee(arguments),Ue=Re[Re.length-1],Be=this._store||this._index,Ke=Be[Oe].apply(Be,Re.slice(0,-1));Ke.onsuccess=function(){Ue(Ke.result)}}})}),[we,De].forEach(function(Oe){Oe.prototype.getAll||(Oe.prototype.getAll=function(We,Re){var Ue=this,Be=[];return new Promise(function(Ke){Ue.iterateCursor(We,function(Fe){return Fe?(Be.push(Fe.value),void 0!==Re&&Be.length==Re?void Ke(Be):void Fe.continue()):void Ke(Be)})})})});var Me={open:function(Oe,We,Re){var Ue=xe(indexedDB,'open',[Oe,We]),Be=Ue.request;return Be.onupgradeneeded=function(Ke){Re&&Re(new je(Be.result,Ke.oldVersion,Be.transaction))},Ue.then(function(Ke){return new Ae(Ke)})},delete:function(Oe){return xe(indexedDB,'deleteDatabase',[Oe])}};be.exports=Me})()});class K{constructor(be,Ee,Ne){if(void 0==be||void 0==Ee||void 0==Ne)throw Error('name, version, storeName must be passed to the constructor.');this._name=be,this._version=Ee,this._storeName=Ne}_getDb(){return this._dbPromise?this._dbPromise:(this._dbPromise=B.open(this._name,this._version,be=>{be.createObjectStore(this._storeName)}).then(be=>{return be}),this._dbPromise)}close(){return this._dbPromise?this._dbPromise.then(be=>{be.close(),this._dbPromise=null}):void 0}put(be,Ee){return this._getDb().then(Ne=>{const xe=Ne.transaction(this._storeName,'readwrite'),qe=xe.objectStore(this._storeName);return qe.put(Ee,be),xe.complete})}delete(be){return this._getDb().then(Ee=>{const Ne=Ee.transaction(this._storeName,'readwrite'),xe=Ne.objectStore(this._storeName);return xe.delete(be),Ne.complete})}get(be){return this._getDb().then(Ee=>{return Ee.transaction(this._storeName).objectStore(this._storeName).get(be)})}getAllValues(){return this._getDb().then(be=>{return be.transaction(this._storeName).objectStore(this._storeName).getAll()})}getAllKeys(){return this._getDb().then(be=>{return be.transaction(this._storeName).objectStore(this._storeName).getAllKeys()})}}const F='v1';let H=`sw-precaching-revisioned-${F}`,L=`sw-precaching-unrevisioned-${F}`;self&&self.registration&&(H+=`-${self.registration.scope}`,L+=`-${self.registration.scope}`);const $=H,G=L;class V{constructor(){this._idbHelper=new K('sw-precaching','1','asset-revisions')}get(be){return this._idbHelper.get(be)}put(be,Ee){return this._idbHelper.put(be,Ee)}_close(){this._idbHelper.close()}}class z{constructor({entryID:be,revision:Ee,request:Ne,cacheBust:xe}){this.entryID=be,this.revision=Ee,this.request=Ne,this.cacheBust=xe}getNetworkRequest(){if(!0!==this.cacheBust)return this.request;let be=this.request.url;const Ee={};if(!0===this.cacheBust)if('cache'in Request.prototype)Ee.cache='reload';else{const Ne=new URL(be,location);Ne.search+=(Ne.search?'&':'')+encodeURIComponent('_sw-precaching')+'='+encodeURIComponent(this.revision),be=Ne.toString()}return new Request(be,Ee)}}class Y extends z{constructor(be){if(q.isType({url:be},'string'),0===be.length)throw W.createError('invalid-revisioned-entry',new Error('Bad url Parameter. It should be a string:'+JSON.stringify(be)));super({entryID:be,revision:be,request:new Request(be),cacheBust:!1})}}class J extends z{constructor({entryID:be,revision:Ee,url:Ne,cacheBust:xe}){if('undefined'==typeof xe&&(xe=!0),'undefined'==typeof be&&(be=new URL(Ne,location).toString()),q.isType({revision:Ee},'string'),0===Ee.length)throw W.createError('invalid-revisioned-entry',new Error('Bad revision Parameter. It should be a string with at least one character: '+JSON.stringify(Ee)));if(q.isType({url:Ne},'string'),0===Ne.length)throw W.createError('invalid-revisioned-entry',new Error('Bad url Parameter. It should be a string:'+JSON.stringify(Ne)));if(q.isType({entryID:be},'string'),0===be.length)throw W.createError('invalid-revisioned-entry',new Error('Bad entryID Parameter. It should be a string with at least one character: '+JSON.stringify(be)));q.isType({cacheBust:xe},'boolean'),super({entryID:be,revision:Ee,request:new Request(Ne),cacheBust:xe})}}class Q extends U{constructor(){super($),this._revisionDetailsModel=new V}cache(be){super.cache(be)}_parseEntry(be){if('undefined'==typeof be||null===be)throw W.createError('invalid-revisioned-entry',new Error('Invalid file entry: '+JSON.stringify(be)));let Ee;switch(typeof be){case'string':Ee=new Y(be);break;case'object':Ee=new J(be);break;default:throw W.createError('invalid-revisioned-entry',new Error('Invalid file entry: '+JSON.stringify(Ee)));}return Ee}_onDuplicateInstallEntryFound(be,Ee){if(Ee.revision!==be.revision)throw W.createError('duplicate-entry-diff-revisions',new Error(`${JSON.stringify(Ee)} <=> `+`${JSON.stringify(be)}`))}_isAlreadyCached(be){var Ee=this;return R(function*(){const Ne=yield Ee._revisionDetailsModel.get(be.entryID);if(Ne!==be.revision)return!1;const xe=yield Ee._getCache(),qe=yield xe.match(be.request);return!!qe})()}_onEntryCached(be){var Ee=this;return R(function*(){yield Ee._revisionDetailsModel.put(be.entryID,be.revision)})()}_close(){this._revisionDetailsModel._close()}}class X extends z{constructor(be){if(!(be instanceof Request))throw W.createError('invalid-unrevisioned-entry',new Error('Invalid file entry: '+JSON.stringify(be)));super({entryID:be.url,request:be,cacheBust:!1})}}class Z extends U{constructor(){super(G)}cache(be){super.cache(be)}_parseEntry(be){if('undefined'==typeof be||null===be)throw W.createError('invalid-unrevisioned-entry',new Error('Invalid file entry: '+JSON.stringify(be)));if('string'==typeof be)return new Y(be);if(be instanceof Request)return new X(be);throw W.createError('invalid-unrevisioned-entry',new Error('Invalid file entry: '+JSON.stringify(be)))}_onDuplicateInstallEntryFound(be,Ee){}_isAlreadyCached(be){return R(function*(){return!1})()}_onEntryCached(be){}}class ee{constructor(){this._eventsRegistered=!1,this._revisionedManager=new Q,this._unrevisionedManager=new Z,this._registerEvents()}_registerEvents(){this._eventsRegistered||(this._eventsRegistered=!0,self.addEventListener('install',be=>{const Ee=Promise.all([this._revisionedManager._performInstallStep(),this._unrevisionedManager._performInstallStep()]).then(()=>{this._close()}).catch(Ne=>{throw this._close(),Ne});be.waitUntil(Ee)}),self.addEventListener('activate',be=>{const Ee=Promise.all([this._revisionedManager._cleanUpOldEntries(),this._unrevisionedManager._cleanUpOldEntries()]).then(()=>{this._close()}).catch(Ne=>{throw this._close(),Ne});be.waitUntil(Ee)}))}cacheRevisioned({revisionedFiles:be}={}){q.isInstance({revisionedFiles:be},Array),this._revisionedManager.cache(be)}cacheUnrevisioned({unrevisionedFiles:be}={}){q.isInstance({unrevisionedFiles:be},Array),this._unrevisionedManager.cache(be)}_close(){this._revisionedManager._close()}}if(!q.isSWEnv())throw W.createError('not-in-sw');const te=`sw-cache-expiration-${self.registration.scope}`,ae='url',ne='timestamp';class se{constructor({maxEntries:be,maxAgeSeconds:Ee}={}){q.atLeastOne({maxEntries:be,maxAgeSeconds:Ee}),void 0!==be&&q.isType({maxEntries:be},'number'),void 0!==Ee&&q.isType({maxAgeSeconds:Ee},'number'),this.maxEntries=be,this.maxAgeSeconds=Ee,this._dbs=new Map,this._caches=new Map}getDB({cacheName:be}){var Ee=this;return R(function*(){if(!Ee._dbs.has(be)){const Ne=yield B.open(te,1,function(xe){const qe=xe.createObjectStore(be,{keyPath:ae});qe.createIndex(ne,ne,{unique:!1})});Ee._dbs.set(be,Ne)}return Ee._dbs.get(be)})()}getCache({cacheName:be}){var Ee=this;return R(function*(){if(!Ee._caches.has(be)){const Ne=yield caches.open(be);Ee._caches.set(be,Ne)}return Ee._caches.get(be)})()}cacheDidUpdate({cacheName:be,newResponse:Ee}={}){q.isType({cacheName:be},'string'),q.isInstance({newResponse:Ee},Response);const Ne=Date.now();this.updateTimestamp({cacheName:be,now:Ne,url:Ee.url}).then(()=>{this.expireEntries({cacheName:be,now:Ne})})}updateTimestamp({cacheName:be,url:Ee,now:Ne}){var xe=this;return R(function*(){q.isType({url:Ee},'string'),'undefined'==typeof Ne&&(Ne=Date.now());const qe=yield xe.getDB({cacheName:be}),Ce=qe.transaction(be,'readwrite');Ce.objectStore(be).put({[ne]:Ne,[ae]:Ee}),yield Ce.complete})()}expireEntries({cacheName:be,now:Ee}={}){var Ne=this;return R(function*(){'undefined'==typeof Ee&&(Ee=Date.now());const xe=Ne.maxAgeSeconds?yield Ne.findOldEntries({cacheName:be,now:Ee}):[],qe=Ne.maxEntries?yield Ne.findExtraEntries({cacheName:be}):[],Ce=[...new Set(xe.concat(qe))];return yield Ne.deleteFromCacheAndIDB({cacheName:be,urls:Ce}),Ce})()}findOldEntries({cacheName:be,now:Ee}={}){var Ne=this;return R(function*(){q.isType({now:Ee},'number');const xe=Ee-1000*Ne.maxAgeSeconds,qe=[],Ce=yield Ne.getDB({cacheName:be}),Ie=Ce.transaction(be,'readonly'),ke=Ie.objectStore(be),Te=ke.index(ne);return Te.iterateCursor(function(we){we&&(we.value[ne]<xe&&qe.push(we.value[ae]),we.continue())}),yield Ie.complete,qe})()}findExtraEntries({cacheName:be}){var Ee=this;return R(function*(){const Ne=[],xe=yield Ee.getDB({cacheName:be}),qe=xe.transaction(be,'readonly'),Ce=qe.objectStore(be),Ie=Ce.index(ne),ke=yield Ie.count();return ke>Ee.maxEntries&&Ie.iterateCursor(function(Te){Te&&(Ne.push(Te.value[ae]),ke-Ne.length>Ee.maxEntries&&Te.continue())}),yield qe.complete,Ne})()}deleteFromCacheAndIDB({cacheName:be,urls:Ee}={}){var Ne=this;return R(function*(){if(q.isInstance({urls:Ee},Array),0<Ee.length){const xe=yield Ne.getCache({cacheName:be}),qe=yield Ne.getDB({cacheName:be});yield Ee.forEach((()=>{var Ce=R(function*(Ie){yield xe.delete(Ie);const ke=qe.transaction(be,'readwrite'),Te=ke.objectStore(be);yield Te.delete(Ie),yield ke.complete});return function(Ie){return Ce.apply(this,arguments)}})())}})()}}const ie=['content-length','etag','last-modified'];class oe{constructor({channelName:be,headersToCheck:Ee,source:Ne}){q.isType({channelName:be},'string'),this.channelName=be,this.headersToCheck=Ee||ie,this.source=Ne||'sw-broadcast-cache-update'}get channel(){return this._channel||(this._channel=new BroadcastChannel(this.channelName)),this._channel}cacheDidUpdate({cacheName:be,oldResponse:Ee,newResponse:Ne}){q.isType({cacheName:be},'string'),q.isInstance({newResponse:Ne},Response),Ee&&this.notifyIfUpdated({cacheName:be,first:Ee,second:Ne})}notifyIfUpdated({first:be,second:Ee,cacheName:Ne}){q.isType({cacheName:Ne},'string'),_({first:be,second:Ee,headersToCheck:this.headersToCheck})||y({cacheName:Ne,url:Ee.url,channel:this.channel,source:this.source})}}const ce=`sw-runtime-caching-${self.registration.scope}`,de=['cacheDidUpdate','cacheWillUpdate','fetchDidFail'];var le=new v({'multiple-cache-will-update-behaviors':'You cannot register more than one behavior that implements cacheWillUpdate.'});class he{constructor({cacheName:be,behaviors:Ee,fetchOptions:Ne,matchOptions:xe}={}){if(be?(q.isType({cacheName:be},'string'),this.cacheName=be):this.cacheName=ce,Ne&&(q.isType({fetchOptions:Ne},'object'),this.fetchOptions=Ne),xe&&(q.isType({matchOptions:xe},'object'),this.matchOptions=xe),this.behaviorCallbacks={},Ee&&(q.isInstance({behaviors:Ee},Array),Ee.forEach(qe=>{for(let Ce of de)'function'==typeof qe[Ce]&&(this.behaviorCallbacks[Ce]||(this.behaviorCallbacks[Ce]=[]),this.behaviorCallbacks[Ce].push(qe[Ce].bind(qe)))})),this.behaviorCallbacks.cacheWillUpdate&&1!==this.behaviorCallbacks.cacheWillUpdate.length)throw le.createError('multiple-cache-will-update-behaviors')}getCache(){var be=this;return R(function*(){return be._cache||(be._cache=yield caches.open(be.cacheName)),be._cache})()}match({request:be}){var Ee=this;return R(function*(){q.atLeastOne({request:be});const Ne=yield Ee.getCache();return yield Ne.match(be,Ee.matchOptions)})()}fetch({request:be}){var Ee=this;return R(function*(){return q.atLeastOne({request:be}),yield fetch(be,Ee.fetchOptions).catch(function(Ne){if(Ee.behaviorCallbacks.fetchDidFail)for(let xe of Ee.behaviorCallbacks.fetchDidFail)xe({request:be});throw Ne})})()}fetchAndCache({request:be,waitOnCache:Ee}){var Ne=this;return R(function*(){q.atLeastOne({request:be});let xe;const qe=yield Ne.fetch({request:be});let Ce=qe.ok;if(Ne.behaviorCallbacks.cacheWillUpdate&&(Ce=Ne.behaviorCallbacks.cacheWillUpdate[0]({request:be,response:qe})),Ce){const Ie=qe.clone();xe=Ne.getCache().then((()=>{var ke=R(function*(Te){let we;'opaque'!==qe.type&&Ne.behaviorCallbacks.cacheDidUpdate&&(we=yield Ne.match({request:be})),yield Te.put(be,Ie);for(let Se of Ne.behaviorCallbacks.cacheDidUpdate||[])Se({cacheName:Ne.cacheName,oldResponse:we,newResponse:Ie})});return function(Te){return ke.apply(this,arguments)}})())}return Ee&&xe&&(yield xe),qe})()}}class pe{constructor({requestWrapper:be}={}){this.requestWrapper=be?be:new he}handle({event:be,params:Ee}={}){throw Error('This abstract method must be implemented in a subclass.')}}class ue extends pe{handle({event:be}={}){var Ee=this;return R(function*(){q.isInstance({event:be},FetchEvent);const Ne=yield Ee.requestWrapper.match({request:be.request});return Ne||(yield Ee.requestWrapper.fetchAndCache({request:be.request}))})()}}class me extends pe{handle({event:be}={}){var Ee=this;return R(function*(){return q.isInstance({event:be},FetchEvent),yield Ee.requestWrapper.match({request:be.request})})()}}class ge extends pe{handle({event:be}={}){var Ee=this;return R(function*(){q.isInstance({event:be},FetchEvent);let Ne;try{if(Ne=yield Ee.requestWrapper.fetchAndCache({request:be.request}),Ne)return Ne}catch(xe){}return yield Ee.requestWrapper.match({request:be.request})})()}}class fe extends pe{handle({event:be}={}){var Ee=this;return R(function*(){return q.isInstance({event:be},FetchEvent),yield Ee.requestWrapper.fetch({request:be.request})})()}}class ye extends pe{handle({event:be}={}){var Ee=this;return R(function*(){q.isInstance({event:be},FetchEvent);const Ne=Ee.requestWrapper.fetchAndCache({request:be.request}).catch(function(){return Response.error()}),xe=yield Ee.requestWrapper.match({request:be.request});return xe||(yield Ne)})()}}class _e{constructor(){this._router=new M,this._precacheManager=new ee}cacheRevisionedAssets(be){if(!Array.isArray(be))throw E.createError('bad-revisioned-cache-list');this._precacheManager.cacheRevisioned({revisionedFiles:be})}warmRuntimeCache(be){if(!Array.isArray(be))throw E.createError('bad-revisioned-cache-list');this._precacheManager.cacheUnrevisioned({unrevisionedFiles:be})}get router(){return this._router}cacheFirst(be){return this._getCachingMechanism(ue,be)}cacheOnly(be){return this._getCachingMechanism(me,be)}networkFirst(be){return this._getCachingMechanism(ge,be)}networkOnly(be){return this._getCachingMechanism(fe,be)}staleWhileRevalidate(be){return this._getCachingMechanism(ye,be)}_getCachingMechanism(be,Ee={}){const Ne={cacheExpiration:se,broadcastCacheUpdate:oe},xe={behaviors:[]};Ee.cacheName&&(xe.cacheName=Ee.cacheName);const qe=Object.keys(Ne);return qe.forEach(Ce=>{if(Ee[Ce]){const Ie=Ne[Ce],ke=Ee[Ce];xe.behaviors.push(new Ie(ke))}}),Ee.behaviors&&Ee.behaviors.forEach(Ce=>{xe.behaviors.push(Ce)}),new be({requestWrapper:new he(xe)})}}if(!q.isSWEnv())throw E.createError('not-in-sw');const ve=new _e;return ve.Route=I,ve});
//# sourceMappingURL=sw-lib.min.js.map
self.__filemanfest = [{url: '/',revision: '1234'}, {url:'/styles/main.css',revision:'12345'}, { . . . . }, { . . . . }, { . . . . }, { . . . . }, { . . . . }];
self.goog.swlib.precache(self.__filemanifest);
from workbox.
(I found #44 (comment) as a reference for some of the past discussion around the same topic.)
I'm not a fan of the rollup bundling because it assumes the developer wants to use ES2015 imports. Some developers use browserify for their JS and in this scenario rollup just adds a complication.
babelify
can be used if a developer want to bundle things themselves, and they're a fan of Browserify. There are a number of alternatives for bundling ES2015 imports, but I suggested that sw-cli
could use Rollup because we're already using it elsewhere in the project.
Finally, I don't think a developer would ever see the version you'd included, if sw-cli uses rollup, the output is:
<big mess of JS>
I'm proposing that the developer would have control over the unbundled JS file, with the one restriction that somewhere within the file, they needed to use ES2015 imports to pull in manifest and sw-lib
.
Here's the flow I'm describing, including how sw-cli
fits in.
The developer starts with:
root/
src/
index.html
image.png
build/
<empty>
The developer runs sw-cli --generate-sw
, and ends up with
root/
src/
index.html
image.png
sw.js
build/
<empty>
src/sw.js
at this point looks like
import manifest from 'path/to/manifest';
import SWLib from 'path/to/sw-lib';
SWLib.cacheRevisionedAssets(manifest);
// As long as the preceding three lines are somewhere in sw.js, the developer
// can do whatever else they want, i.e. use SWLib to set up routes, use push
// messaging, whatever.
At that point, as part of their build process, they need to run sw-cli
to generate an up to date build/manifest.js
(we'd need to switch to .js
instead of .json
so that it could be used as an ES2015 import). As part of this same build step, sw-cli
could include an option to use a module bundler like Rollup on src/sw.js
, for developers who would rather we take care of it for them. Or developers could bundle src/sw.js
using a different build tool if they want. Either way, you'd end up with:
root/
src/
index.html
image.png
sw.js
build/
index.html
image.png
sw.js
manifest.js
build/sw.js
will contain all that bundled JS that you included in your previous comment, but that's fine—the developer doesn't interact with build/sw.js
. They could inspect and modify and do whatever they'd like with src/sw.js
. src/sw.js
is not modified during the build process so their modifications won't be overwritten.
(FWIW, build/manifest.js
doesn't actually need to be deployed to a web server with this approach, but it needs to be stored somewhere.)
from workbox.
I suppose I'm just disappointed that this approach requires a build process (i.e. it's not a usable service worker). I'd rather not have the CLI build the service worker at it and just build the file manifest then developers figure out what they do with it.
This would skip the complication of explaining the build step developers need to add and get them to use their current build process (Or find some other way of using the manifest - i.e. inline the manifest with a custom regex).
from workbox.
The manifest needs to be regenerated as part of a build process prior to deployment, so even apart from this discussion, I don't see how we could support a use case in which there is no build process.
If we offer an "easy mode" that hides the details about bundling within sw-cli
then the manifest generation + bundling process won't be more of a burden than just manifest generation, from the perspective of an end user.
from workbox.
Yeah I'm gonna leave this for a few days and come back to it on Monday.
It feels like approaching this from an angle of Module + CLI to cover your use case, the current all-in-one sw-precache use case & some clear path from one to the other is there, but the details is where we are diverting...sorry for all the noise today.
from workbox.
- An API for generating SW when used as a node-module.
- An API for generating a manifest used as a node-module.
- Add in support for creating and reading in the config file for CLI and node module.
- Add in support for building manifest from CLI (either as Questions or config)
from workbox.
Assigning this work to the Sydney beta as having a functional CLI ready by that time will be necessary for folks to provide feedback on the workflow we're hoping to prescribe for the longtail of users.
@jeffposnick @gauntface I know we've had a few sw-goog syncs lately that touched on the right strategy here. What's your perspective on where we've landed? Is there still any contention remaining?
It feels like approaching this from an angle of Module + CLI to cover your use case, the current all-in-one sw-precache use case & some clear path from one to the other is there, but the details is where we are diverting...sorry for all the noise today.
Fwiw, a lot of the folks I've seen use sw-precache (especially on the framework front) will treat it as a post-build-pipeline tool that is just run from an npm script
. This workflow means treating the CLI tool as something that will split out a Service Worker that requires no further build tooling of its own in order to function. The increase in availability of Webpack (and so on) plugins may change this, but I'd been keen to see if we can preserve supporting the no additional build process flow.
from workbox.
Going to close this issue as I feel future changes and issues should be tracked in their own issues.
from workbox.
Related Issues (20)
- CacheableResponse should accept an array of header values
- Precaching doesn't work properly in Safari when offline
- Consider adding `maxUnusedSeconds` and `updateStale` to `ExpirationPlugin` config
- Standardize behavior of `maxAgeSeconds`
- Update Node version on Glitch demos
- Support wasm files by default in globPatterns
- CVE-2021-23337 Command Injection in lodash
- [docs] Wrong menu label for page "Retrying requests when back online"
- Minimum rollup version isn't high enough for @rollup/plugin-node-resolve
- replace rollup-plugin-off-main-thread in workbox-build to fix deprecation warnings
- If you use "speculationrules" workbox multiplies caches with the same name every time the user navigates to a page that has been "pre-rendered"
- 'Got' dependency has security issue (dependabot) HOT 1
- 🐛BackgroundSyncPlugin doesn't sync when connection is back
- Credentials missing in navigate requests HOT 1
- How to correctly use of precacheAndRoute to serve index.html ? (how to precache current html?)
- [DELETED]
- ł
- CVE-2023-29827
- FetchEvent.respondWith received an error - for Google Tag Manager (GTM) script
- FetchEvent.respondWith received an error: ReferenceError: Can't find variable: FetchEvent
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from workbox.