diff --git a/infra/testing/validator/service-worker-runtime.js b/infra/testing/validator/service-worker-runtime.js index 47cba26bb..1a3422ac9 100644 --- a/infra/testing/validator/service-worker-runtime.js +++ b/infra/testing/validator/service-worker-runtime.js @@ -33,6 +33,7 @@ function setupSpiesAndContext() { // To make testing easier, return the name of the strategy. strategies: { cacheFirst: sinon.stub().returns('cacheFirst'), + networkFirst: sinon.stub().returns('networkFirst'), }, }; @@ -47,6 +48,7 @@ function setupSpiesAndContext() { cacheExpirationPlugin: workbox.expiration.Plugin, cacheFirst: workbox.strategies.cacheFirst, clientsClaim: workbox.clientsClaim, + networkFirst: workbox.strategies.networkFirst, precacheAndRoute: workbox.precaching.precacheAndRoute, registerNavigationRoute: workbox.routing.registerNavigationRoute, registerRoute: workbox.routing.registerRoute, diff --git a/packages/workbox-build/src/entry-points/options/common-generate-schema.js b/packages/workbox-build/src/entry-points/options/common-generate-schema.js index 1858935ec..cf61700e0 100644 --- a/packages/workbox-build/src/entry-points/options/common-generate-schema.js +++ b/packages/workbox-build/src/entry-points/options/common-generate-schema.js @@ -38,16 +38,25 @@ module.exports = baseSchema.keys({ 'staleWhileRevalidate' )], options: joi.object().keys({ - cacheName: joi.string(), - plugins: joi.array().items(joi.object()), - cacheExpiration: joi.object().keys({ - maxEntries: joi.number().min(1), - maxAgeSeconds: joi.number().min(1), - }).or('maxEntries', 'maxAgeSeconds'), + backgroundSync: joi.object().keys({ + name: joi.string().required(), + options: joi.object(), + }), + broadcastCacheUpdate: joi.object().keys({ + channelName: joi.string().required(), + options: joi.object(), + }), cacheableResponse: joi.object().keys({ statuses: joi.array().items(joi.number().min(0).max(599)), headers: joi.object(), }).or('statuses', 'headers'), + cacheName: joi.string(), + expiration: joi.object().keys({ + maxEntries: joi.number().min(1), + maxAgeSeconds: joi.number().min(1), + }).or('maxEntries', 'maxAgeSeconds'), + networkTimeoutSeconds: joi.number().min(1), + plugins: joi.array().items(joi.object()), }), }).requiredKeys('urlPattern', 'handler')), skipWaiting: joi.boolean().default(defaults.skipWaiting), diff --git a/packages/workbox-build/src/lib/errors.js b/packages/workbox-build/src/lib/errors.js index c20abd1c5..a80c818bd 100644 --- a/packages/workbox-build/src/lib/errors.js +++ b/packages/workbox-build/src/lib/errors.js @@ -105,4 +105,6 @@ module.exports = { supported.)`, 'bad-runtime-caching-config': ol`An unknown configuration option was used with runtimeCaching:`, + 'invalid-network-timeout-seconds': ol`When using networkTimeoutSeconds, you + must set the handler to 'networkFirst'.`, }; diff --git a/packages/workbox-build/src/lib/runtime-caching-converter.js b/packages/workbox-build/src/lib/runtime-caching-converter.js index fb74f26ec..1a55c5a5e 100644 --- a/packages/workbox-build/src/lib/runtime-caching-converter.js +++ b/packages/workbox-build/src/lib/runtime-caching-converter.js @@ -35,29 +35,17 @@ function getOptionsString(options = {}) { delete options.plugins; } - let cacheName; - if (options.cache && options.cache.name) { - cacheName = options.cache.name; - delete options.cache.name; - } - - // Allow a top-level cacheName value to override the cache.name value. - if (options.cacheName) { - cacheName = options.cacheName; - delete options.cacheName; - } - - let networkTimeoutSeconds; - if (options.networkTimeoutSeconds) { - networkTimeoutSeconds = options.networkTimeoutSeconds; - delete options.networkTimeoutSeconds; - } + // Pull cacheName and networkTimeoutSeconds from the options object, since + // they are not directly used to construct a Plugin instance. + // If set, need to be passed as options to the handler constructor instead. + const {cacheName, networkTimeoutSeconds} = options; + delete options.cacheName; + delete options.networkTimeoutSeconds; const pluginsMapping = { backgroundSync: 'workbox.backgroundSync.Plugin', broadcastCacheUpdate: 'workbox.broadcastCacheUpdate.Plugin', - cache: 'workbox.expiration.Plugin', - cacheExpiration: 'workbox.expiration.Plugin', + expiration: 'workbox.expiration.Plugin', cacheableResponse: 'workbox.cacheableResponse.Plugin', }; @@ -87,8 +75,7 @@ function getOptionsString(options = {}) { } } -module.exports = (runtimeCaching) => { - runtimeCaching = runtimeCaching || []; +module.exports = (runtimeCaching = []) => { return runtimeCaching.map((entry) => { const method = entry.method || 'GET'; @@ -100,6 +87,13 @@ module.exports = (runtimeCaching) => { throw new Error(errors['handler-is-required']); } + // This validation logic is a bit too gnarly for joi, so it's manually + // implemented here. + if (entry.options && entry.options.networkTimeoutSeconds && + entry.handler !== 'networkFirst') { + throw new Error(errors['invalid-network-timeout-seconds']); + } + // urlPattern might be either a string or a RegExp object. // If it's a string, it needs to be quoted. If it's a RegExp, it should // be used as-is. diff --git a/test/workbox-build/node/entry-points/generate-sw-string.js b/test/workbox-build/node/entry-points/generate-sw-string.js index d831a348a..e2e42afc9 100644 --- a/test/workbox-build/node/entry-points/generate-sw-string.js +++ b/test/workbox-build/node/entry-points/generate-sw-string.js @@ -366,7 +366,7 @@ describe(`[workbox-build] entry-points/generate-sw-string.js (End to End)`, func const runtimeCachingOptions = { cacheName: 'test-cache-name', plugins: [{}, {}], - cacheExpiration: { + expiration: { maxEntries: 1, maxAgeSeconds: 1, }, @@ -395,7 +395,7 @@ describe(`[workbox-build] entry-points/generate-sw-string.js (End to End)`, func ]), }]], cacheableResponsePlugin: [[runtimeCachingOptions.cacheableResponse]], - cacheExpirationPlugin: [[runtimeCachingOptions.cacheExpiration]], + cacheExpirationPlugin: [[runtimeCachingOptions.expiration]], importScripts: [[...DEFAULT_IMPORT_SCRIPTS]], suppressWarnings: [[]], precacheAndRoute: [[[], {}]], @@ -406,7 +406,7 @@ describe(`[workbox-build] entry-points/generate-sw-string.js (End to End)`, func it(`should support setting individual 'options' each, for multiple 'runtimeCaching' entries`, async function() { const firstRuntimeCachingOptions = { cacheName: 'first-cache-name', - cacheExpiration: { + expiration: { maxEntries: 1, maxAgeSeconds: 1, }, @@ -442,7 +442,7 @@ describe(`[workbox-build] entry-points/generate-sw-string.js (End to End)`, func plugins: ['workbox.cacheableResponse.Plugin'], }]], cacheableResponsePlugin: [[secondRuntimeCachingOptions.cacheableResponse]], - cacheExpirationPlugin: [[firstRuntimeCachingOptions.cacheExpiration]], + cacheExpirationPlugin: [[firstRuntimeCachingOptions.expiration]], importScripts: [[...DEFAULT_IMPORT_SCRIPTS]], suppressWarnings: [[]], precacheAndRoute: [[[], {}]], diff --git a/test/workbox-build/node/entry-points/generate-sw.js b/test/workbox-build/node/entry-points/generate-sw.js index 15d136f38..767b0919a 100644 --- a/test/workbox-build/node/entry-points/generate-sw.js +++ b/test/workbox-build/node/entry-points/generate-sw.js @@ -6,6 +6,7 @@ const tempy = require('tempy'); const cdnUtils = require('../../../../packages/workbox-build/src/lib/cdn-utils'); const copyWorkboxLibraries = require('../../../../packages/workbox-build/src/lib/copy-workbox-libraries'); +const errors = require('../../../../packages/workbox-build/src/lib/errors'); const generateSW = require('../../../../packages/workbox-build/src/entry-points/generate-sw'); const validateServiceWorkerRuntime = require('../../../../infra/testing/validator/service-worker-runtime'); @@ -551,7 +552,7 @@ describe(`[workbox-build] entry-points/generate-sw.js (End to End)`, function() const swDest = tempy.file(); const firstRuntimeCachingOptions = { cacheName: 'first-cache-name', - cacheExpiration: { + expiration: { maxEntries: 1, maxAgeSeconds: 1, }, @@ -592,7 +593,7 @@ describe(`[workbox-build] entry-points/generate-sw.js (End to End)`, function() plugins: ['workbox.cacheableResponse.Plugin'], }]], cacheableResponsePlugin: [[secondRuntimeCachingOptions.cacheableResponse]], - cacheExpirationPlugin: [[firstRuntimeCachingOptions.cacheExpiration]], + cacheExpirationPlugin: [[firstRuntimeCachingOptions.expiration]], importScripts: [[WORKBOX_SW_CDN_URL]], suppressWarnings: [[]], precacheAndRoute: [[[{ @@ -620,5 +621,80 @@ describe(`[workbox-build] entry-points/generate-sw.js (End to End)`, function() ], }}); }); + + it(`should reject with a ValidationError when 'networkTimeoutSeconds' is used and handler is not 'networkFirst'`, async function() { + const swDest = tempy.file(); + const runtimeCachingOptions = { + networkTimeoutSeconds: 1, + }; + const runtimeCaching = [{ + urlPattern: REGEXP_URL_PATTERN, + handler: 'networkOnly', + options: runtimeCachingOptions, + }]; + const options = Object.assign({}, BASE_OPTIONS, { + runtimeCaching, + swDest, + }); + + try { + await generateSW(options); + throw new Error('Unexpected success.'); + } catch (error) { + expect(error.message).to.include(errors['invalid-network-timeout-seconds']); + } + }); + + it(`should support 'networkTimeoutSeconds' when handler is 'networkFirst'`, async function() { + const swDest = tempy.file(); + const networkTimeoutSeconds = 1; + const handler = 'networkFirst'; + + const runtimeCachingOptions = { + networkTimeoutSeconds, + plugins: [], + }; + const runtimeCaching = [{ + urlPattern: REGEXP_URL_PATTERN, + handler, + options: runtimeCachingOptions, + }]; + const options = Object.assign({}, BASE_OPTIONS, { + runtimeCaching, + swDest, + }); + + const {count, size} = await generateSW(options); + + expect(count).to.eql(6); + expect(size).to.eql(2421); + await validateServiceWorkerRuntime({swFile: swDest, expectedMethodCalls: { + [handler]: [[runtimeCachingOptions]], + importScripts: [[WORKBOX_SW_CDN_URL]], + suppressWarnings: [[]], + precacheAndRoute: [[[{ + url: 'index.html', + revision: '3883c45b119c9d7e9ad75a1b4a4672ac', + }, { + url: 'page-1.html', + revision: '544658ab25ee8762dc241e8b1c5ed96d', + }, { + url: 'page-2.html', + revision: 'a3a71ce0b9b43c459cf58bd37e911b74', + }, { + url: 'styles/stylesheet-1.css', + revision: '934823cbc67ccf0d67aa2a2eeb798f12', + }, { + url: 'styles/stylesheet-2.css', + revision: '884f6853a4fc655e4c2dc0c0f27a227c', + }, { + url: 'webpackEntry.js', + revision: 'd41d8cd98f00b204e9800998ecf8427e', + }], {}]], + registerRoute: [ + [REGEXP_URL_PATTERN, handler, DEFAULT_METHOD], + ], + }}); + }); }); }); diff --git a/test/workbox-build/node/lib/runtime-caching-converter.js b/test/workbox-build/node/lib/runtime-caching-converter.js index c83e9e365..ffea8debe 100644 --- a/test/workbox-build/node/lib/runtime-caching-converter.js +++ b/test/workbox-build/node/lib/runtime-caching-converter.js @@ -65,15 +65,12 @@ function validate(runtimeCachingOptions, convertedOptions) { .to.eql(strategiesOptions.networkTimeoutSeconds); } - if (options.cache) { - if (options.cache.name) { - expect(strategiesOptions.cacheName).to.eql(options.cache.name); - delete options.cache.name; - } - - if (Object.keys(options.cache).length > 0) { - expect(globalScope.workbox.expiration.Plugin.calledWith(options.cache)).to.be.true; - } + if (options.cacheName) { + expect(options.cacheName).to.eql(strategiesOptions.cacheName); + } + + if (Object.keys(options.expiration).length > 0) { + expect(globalScope.workbox.expiration.Plugin.calledWith(options.expiration)).to.be.true; } if (options.cacheableResponse) { @@ -127,8 +124,8 @@ describe(`[workbox-build] src/lib/utils/runtime-caching-converter.js`, function( handler: 'networkFirst', options: { networkTimeoutSeconds: 20, - cache: { - name: 'abc-cache', + cacheName: 'abc-cache', + expiration: { maxEntries: 5, maxAgeSeconds: 50, }, @@ -137,7 +134,7 @@ describe(`[workbox-build] src/lib/utils/runtime-caching-converter.js`, function( urlPattern: '/test', handler: 'staleWhileRevalidate', options: { - cache: { + expiration: { maxEntries: 10, }, cacheableResponse: {