-
Notifications
You must be signed in to change notification settings - Fork 830
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SW-CLI Usage #82
Comments
Looking at the |
This is shaping up really well. Great work, @gauntface.
|
|
Note to self: Add tool to notify of CLI update. |
@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 |
Yeah that's what I intended to add in. |
Drive-by feedback:
|
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. |
Use cases for CLI
Generate SW + ManifestGoals of this in my head are to:
Option 1This follows what most of the team are doing for their service workers. The downsides of this approach are:
importScripts('/sw-lib.12345.min.js');
const fileManifest = [
{
url: '/',
revision: '1234'
},
{
....
}
];
self.goog.swlib.cacheRevisionedAssets(fileManifest); Option 2Alternative, but goes against what everyone does naturally is to have the file manifest. Downsides:
importScripts(
'/precache-manifest.12345.js',
'/sw-lib.12345.min.js'
);
self.goog.swlib.cacheRevisionedAssets(self.__file_manifest); Generate File Manifest OnlyGoals of this in my head are to:
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 SWThis 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.
|
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. |
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 The initial bootstrapping can optionally create a 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 I think it would make sense if Once there's widely deployed support for 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 |
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); |
(I found #44 (comment) as a reference for some of the past discussion around the same topic.)
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 Here's the flow I'm describing, including how The developer starts with:
The developer runs
At that point, as part of their build process, they need to run
(FWIW, |
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). |
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 |
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. |
|
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?
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 |
Going to close this issue as I feel future changes and issues should be tracked in their own issues. |
@jeffposnick @addyosmani
Sorry I haven't covered this topic sooner - it's only lightly covered in the sw-goog doc.
I've started to consider the scenarios of the CLI usage. If we can cover the primary use cases in the CLI, I'm confident the right API can be exposed as a module for gulp and grunt etc.
In the doc we outline the main use cases as generating a service worker and building a file manifest.
Generating a Service Worker
The user journeys of this are likely to be:
First Use -
sw-cli generate-sw
For the first use we can use an interactive CLI that guides the developer somewhat, asking them for:
The result of this is a service worker and file manifest. If the user selects to generate a config file, it'll also be generated and the config will include a more extensive set of config options (i.e. options that are excluded from the interactive CLI)..
Second Use -
sw-cli generate-sw
The second flow would go through the same flow:
1.) If a config file is found with name sw-cli-config.json then we'll ask the developer if they wish to use it.
2.) If not found or used, the above flow will be taken
3.) We will ask the developer if they wish to override the service worker file and / or config file if required.
CI Use -
sw-cli generate-sw --non-interactive
This mode will require the default config file to exist on the current working directory OR expect the '--config' flag to be set. Overwriting tan existing service worker file will be blocked by default but --force can be used to force it (I'd expect developers to clean the directory before overwriting).
Build File Manifest
The manifest can use the same config file from the previous step and could follow the same interactive flow:
The second use and CI use would follow the same pattern as above. The only difference is any questions regarding SW are skipped.
Does this seem like a reasonable first approach?
The text was updated successfully, but these errors were encountered: