From dde16bab084394666c9ae266e664cf7f762340fc Mon Sep 17 00:00:00 2001 From: Jeffrey Posnick Date: Thu, 4 Jan 2018 10:50:14 -0500 Subject: [PATCH] Add in asset filename filtering via {test, include, exclude} (#1149) * Add in filtering via {test, include, exclude} options. * Updated a comment. --- package-lock.json | 19 +- package.json | 1 + packages/workbox-webpack-plugin/package.json | 3 + .../workbox-webpack-plugin/src/generate-sw.js | 6 + .../src/inject-manifest.js | 6 + .../get-manifest-entries-from-compilation.js | 9 + .../src/lib/sanitize-config.js | 6 + .../node/generate-sw.js | 248 +++++++++++++++++ .../node/inject-manifest.js | 253 ++++++++++++++++++ 9 files changed, 538 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index f2f4b7fb6..5efc42dbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7181,6 +7181,12 @@ } } }, + "generate-asset-webpack-plugin": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/generate-asset-webpack-plugin/-/generate-asset-webpack-plugin-0.3.0.tgz", + "integrity": "sha1-ZmLvgDP21DMMeImBZGHOJWgxMxg=", + "dev": true + }, "get-caller-file": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", @@ -16474,19 +16480,6 @@ "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", "dev": true }, - "shelving-mock-event": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/shelving-mock-event/-/shelving-mock-event-1.0.12.tgz", - "integrity": "sha512-2F+IZ010rwV3sA/Kd2hnC1vGNycsxeBJmjkXR8+4IOlv5e+Wvj+xH+A8Cv8/Z0lUyCut/HcxSpeDccYTVtnuaQ==", - "dev": true - }, - "shelving-mock-indexeddb": { - "version": "github:philipwalton/shelving-mock-indexeddb#594ec7fa09a910019fd6e5fa22128d02a0e14a55", - "dev": true, - "requires": { - "shelving-mock-event": "1.0.12" - } - }, "sigmund": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", diff --git a/package.json b/package.json index c1b3be152..5f8a425a4 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "firebase-tools": "^3.13.1", "fs-extra": "^4.0.1", "geckodriver": "^1.8.1", + "generate-asset-webpack-plugin": "^0.3.0", "github": "^12.0.1", "glob": "^7.1.2", "gulp": "github:gulpjs/gulp#4.0", diff --git a/packages/workbox-webpack-plugin/package.json b/packages/workbox-webpack-plugin/package.json index 17e60bb5c..26d78dd29 100644 --- a/packages/workbox-webpack-plugin/package.json +++ b/packages/workbox-webpack-plugin/package.json @@ -31,6 +31,9 @@ "json-stable-stringify": "^1.0.1", "workbox-build": "^3.0.0-alpha.3" }, + "peerDependencies": { + "webpack": "^2.0.0 || ^3.0.0" + }, "author": "Will Farley (http://www.willfarley.org/)", "license": "Apache-2.0", "repository": "googlechrome/workbox", diff --git a/packages/workbox-webpack-plugin/src/generate-sw.js b/packages/workbox-webpack-plugin/src/generate-sw.js index e345ee0c3..09f0c8fd6 100644 --- a/packages/workbox-webpack-plugin/src/generate-sw.js +++ b/packages/workbox-webpack-plugin/src/generate-sw.js @@ -45,6 +45,12 @@ class GenerateSW { constructor(config = {}) { this.config = Object.assign({}, { chunks: [], + exclude: [ + // Exclude source maps. + /\.map$/, + // Exclude anything starting with manifest and ending .js or .json. + /^manifest.*\.js(?:on)?$/, + ], excludeChunks: [], importScripts: [], importWorkboxFrom: 'cdn', diff --git a/packages/workbox-webpack-plugin/src/inject-manifest.js b/packages/workbox-webpack-plugin/src/inject-manifest.js index 12b630802..e87127d9b 100644 --- a/packages/workbox-webpack-plugin/src/inject-manifest.js +++ b/packages/workbox-webpack-plugin/src/inject-manifest.js @@ -52,6 +52,12 @@ class InjectManifest { this.config = Object.assign({}, { chunks: [], + exclude: [ + // Exclude source maps. + /\.map$/, + // Exclude anything starting with manifest and ending .js or .json. + /^manifest.*\.js(?:on)?$/, + ], excludeChunks: [], importScripts: [], importWorkboxFrom: 'cdn', diff --git a/packages/workbox-webpack-plugin/src/lib/get-manifest-entries-from-compilation.js b/packages/workbox-webpack-plugin/src/lib/get-manifest-entries-from-compilation.js index edd806105..fdbce0581 100644 --- a/packages/workbox-webpack-plugin/src/lib/get-manifest-entries-from-compilation.js +++ b/packages/workbox-webpack-plugin/src/lib/get-manifest-entries-from-compilation.js @@ -14,6 +14,8 @@ limitations under the License. */ +const ModuleFilenameHelpers = require('webpack/lib/ModuleFilenameHelpers'); + const getAssetHash = require('./get-asset-hash'); const resolveWebpackUrl = require('./resolve-webpack-url'); @@ -177,6 +179,13 @@ function getManifestEntriesFromCompilation(compilation, config) { const manifestEntries = []; for (const [file, metadata] of Object.entries(filteredAssetMetadata)) { + // Filter based on test/include/exclude options set in the config, + // following webpack's conventions. + // This matches the behavior of, e.g., UglifyJS's webpack plugin. + if (!ModuleFilenameHelpers.matchObject(config, file)) { + continue; + } + const publicUrl = resolveWebpackUrl(publicPath, file); const manifestEntry = getEntry(knownHashes, publicUrl, metadata.hash); manifestEntries.push(manifestEntry); diff --git a/packages/workbox-webpack-plugin/src/lib/sanitize-config.js b/packages/workbox-webpack-plugin/src/lib/sanitize-config.js index d9452235f..0a8bce66b 100644 --- a/packages/workbox-webpack-plugin/src/lib/sanitize-config.js +++ b/packages/workbox-webpack-plugin/src/lib/sanitize-config.js @@ -25,11 +25,14 @@ function forGetManifest(originalConfig) { const propertiesToRemove = [ 'chunks', + 'exclude', 'excludeChunks', 'importScripts', 'importWorkboxFrom', + 'include', 'swDest', 'swSrc', + 'test', ]; return sanitizeConfig(originalConfig, propertiesToRemove); @@ -46,9 +49,12 @@ function forGetManifest(originalConfig) { function forGenerateSWString(originalConfig) { const propertiesToRemove = [ 'chunks', + 'exclude', 'excludeChunks', 'importWorkboxFrom', + 'include', 'swDest', + 'test', ]; return sanitizeConfig(originalConfig, propertiesToRemove); diff --git a/test/workbox-webpack-plugin/node/generate-sw.js b/test/workbox-webpack-plugin/node/generate-sw.js index 6bb1c4e0f..5b70d5dc2 100644 --- a/test/workbox-webpack-plugin/node/generate-sw.js +++ b/test/workbox-webpack-plugin/node/generate-sw.js @@ -1,4 +1,5 @@ const CopyWebpackPlugin = require('copy-webpack-plugin'); +const GenerateAssetPlugin = require('generate-asset-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const expect = require('chai').expect; const fse = require('fs-extra'); @@ -655,4 +656,251 @@ describe(`[workbox-webpack-plugin] GenerateSW (End to End)`, function() { }); }); }); + + describe(`[workbox-webpack-plugin] Filtering via test/include/exclude`, function() { + const TEST_ASSET_CB = (compilation, callback) => callback(null, 'test'); + + it(`should exclude .map and manifest.js(on) files by default`, function(done) { + const FILE_MANIFEST_NAME = 'precache-manifest.43591bdf46c7ac47eb8d7b2bcd41f13e.js'; + const outputDir = tempy.directory(); + const config = { + entry: path.join(SRC_DIR, WEBPACK_ENTRY_FILENAME), + output: { + filename: WEBPACK_ENTRY_FILENAME, + path: outputDir, + }, + devtool: 'source-map', + plugins: [ + new GenerateAssetPlugin({ + filename: 'manifest.js', + fn: TEST_ASSET_CB, + }), + new GenerateAssetPlugin({ + filename: 'manifest.json', + fn: TEST_ASSET_CB, + }), + new GenerateSW(), + ], + }; + + const compiler = webpack(config); + compiler.run(async (webpackError) => { + if (webpackError) { + return done(webpackError); + } + + const swFile = path.join(outputDir, 'service-worker.js'); + try { + // First, validate that the generated service-worker.js meets some basic assumptions. + await validateServiceWorkerRuntime({swFile, expectedMethodCalls: { + importScripts: [[ + FILE_MANIFEST_NAME, + WORKBOX_SW_FILE_NAME, + ]], + suppressWarnings: [[]], + precacheAndRoute: [[[], {}]], + }}); + + // Next, test the generated manifest to ensure that it contains + // exactly the entries that we expect. + const manifestFileContents = await fse.readFile(path.join(outputDir, FILE_MANIFEST_NAME), 'utf-8'); + const context = {self: {}}; + vm.runInNewContext(manifestFileContents, context); + + const expectedEntries = [{ + revision: '8e8e9f093f036bd18dfa', + url: 'webpackEntry.js', + }]; + expect(context.self.__precacheManifest).to.eql(expectedEntries); + + done(); + } catch (error) { + done(error); + } + }); + }); + + it(`should allow developers to override the default exclude filter`, function(done) { + const FILE_MANIFEST_NAME = 'precache-manifest.b87ef85173e527290f94979b09e72d12.js'; + const outputDir = tempy.directory(); + const config = { + entry: path.join(SRC_DIR, WEBPACK_ENTRY_FILENAME), + output: { + filename: WEBPACK_ENTRY_FILENAME, + path: outputDir, + }, + devtool: 'source-map', + plugins: [ + new GenerateSW({ + exclude: [], + }), + ], + }; + + const compiler = webpack(config); + compiler.run(async (webpackError) => { + if (webpackError) { + return done(webpackError); + } + + const swFile = path.join(outputDir, 'service-worker.js'); + try { + // First, validate that the generated service-worker.js meets some basic assumptions. + await validateServiceWorkerRuntime({swFile, expectedMethodCalls: { + importScripts: [[ + FILE_MANIFEST_NAME, + WORKBOX_SW_FILE_NAME, + ]], + suppressWarnings: [[]], + precacheAndRoute: [[[], {}]], + }}); + + // Next, test the generated manifest to ensure that it contains + // exactly the entries that we expect. + const manifestFileContents = await fse.readFile(path.join(outputDir, FILE_MANIFEST_NAME), 'utf-8'); + const context = {self: {}}; + vm.runInNewContext(manifestFileContents, context); + + const expectedEntries = [{ + revision: '8e8e9f093f036bd18dfa', + url: 'webpackEntry.js.map', + }, { + revision: '8e8e9f093f036bd18dfa', + url: 'webpackEntry.js', + }]; + expect(context.self.__precacheManifest).to.eql(expectedEntries); + + done(); + } catch (error) { + done(error); + } + }); + }); + + it(`should allow developers to whitelist via include`, function(done) { + const FILE_MANIFEST_NAME = 'precache-manifest.1242f9e007897f035a52e56690ff17a6.js'; + const outputDir = tempy.directory(); + const config = { + entry: path.join(SRC_DIR, WEBPACK_ENTRY_FILENAME), + output: { + filename: WEBPACK_ENTRY_FILENAME, + path: outputDir, + }, + devtool: 'source-map', + plugins: [ + new CopyWebpackPlugin([{ + from: SRC_DIR, + to: outputDir, + }]), + new GenerateSW({ + include: [/.html$/], + }), + ], + }; + + const compiler = webpack(config); + compiler.run(async (webpackError) => { + if (webpackError) { + return done(webpackError); + } + + const swFile = path.join(outputDir, 'service-worker.js'); + try { + // First, validate that the generated service-worker.js meets some basic assumptions. + await validateServiceWorkerRuntime({swFile, expectedMethodCalls: { + importScripts: [[ + FILE_MANIFEST_NAME, + WORKBOX_SW_FILE_NAME, + ]], + suppressWarnings: [[]], + precacheAndRoute: [[[], {}]], + }}); + + // Next, test the generated manifest to ensure that it contains + // exactly the entries that we expect. + const manifestFileContents = await fse.readFile(path.join(outputDir, FILE_MANIFEST_NAME), 'utf-8'); + const context = {self: {}}; + vm.runInNewContext(manifestFileContents, context); + + const expectedEntries = [{ + revision: 'a3a71ce0b9b43c459cf58bd37e911b74', + url: 'page-2.html', + }, { + revision: '544658ab25ee8762dc241e8b1c5ed96d', + url: 'page-1.html', + }, { + revision: '3883c45b119c9d7e9ad75a1b4a4672ac', + url: 'index.html', + }]; + expect(context.self.__precacheManifest).to.eql(expectedEntries); + + done(); + } catch (error) { + done(error); + } + }); + }); + + it(`should allow developers to combine the test and exclude filters`, function(done) { + const FILE_MANIFEST_NAME = 'precache-manifest.83df65831eb442f8ad5fbeb8edecc558.js'; + const outputDir = tempy.directory(); + const config = { + entry: path.join(SRC_DIR, WEBPACK_ENTRY_FILENAME), + output: { + filename: WEBPACK_ENTRY_FILENAME, + path: outputDir, + }, + devtool: 'source-map', + plugins: [ + new CopyWebpackPlugin([{ + from: SRC_DIR, + to: outputDir, + }]), + new GenerateSW({ + test: [/.html$/], + exclude: [/index/], + }), + ], + }; + + const compiler = webpack(config); + compiler.run(async (webpackError) => { + if (webpackError) { + return done(webpackError); + } + + const swFile = path.join(outputDir, 'service-worker.js'); + try { + // First, validate that the generated service-worker.js meets some basic assumptions. + await validateServiceWorkerRuntime({swFile, expectedMethodCalls: { + importScripts: [[ + FILE_MANIFEST_NAME, + WORKBOX_SW_FILE_NAME, + ]], + suppressWarnings: [[]], + precacheAndRoute: [[[], {}]], + }}); + + // Next, test the generated manifest to ensure that it contains + // exactly the entries that we expect. + const manifestFileContents = await fse.readFile(path.join(outputDir, FILE_MANIFEST_NAME), 'utf-8'); + const context = {self: {}}; + vm.runInNewContext(manifestFileContents, context); + + const expectedEntries = [{ + revision: 'a3a71ce0b9b43c459cf58bd37e911b74', + url: 'page-2.html', + }, { + revision: '544658ab25ee8762dc241e8b1c5ed96d', + url: 'page-1.html', + }]; + expect(context.self.__precacheManifest).to.eql(expectedEntries); + + done(); + } catch (error) { + done(error); + } + }); + }); + }); }); diff --git a/test/workbox-webpack-plugin/node/inject-manifest.js b/test/workbox-webpack-plugin/node/inject-manifest.js index 88d7426b7..5ab721e36 100644 --- a/test/workbox-webpack-plugin/node/inject-manifest.js +++ b/test/workbox-webpack-plugin/node/inject-manifest.js @@ -1,4 +1,5 @@ const CopyWebpackPlugin = require('copy-webpack-plugin'); +const GenerateAssetPlugin = require('generate-asset-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const expect = require('chai').expect; const fse = require('fs-extra'); @@ -649,4 +650,256 @@ describe(`[workbox-webpack-plugin] InjectManifest (End to End)`, function() { }); }); }); + + describe(`[workbox-webpack-plugin] Filtering via test/include/exclude`, function() { + const TEST_ASSET_CB = (compilation, callback) => callback(null, 'test'); + + it(`should exclude .map and manifest.js(on) files by default`, function(done) { + const FILE_MANIFEST_NAME = 'precache-manifest.43591bdf46c7ac47eb8d7b2bcd41f13e.js'; + const outputDir = tempy.directory(); + const config = { + entry: path.join(SRC_DIR, WEBPACK_ENTRY_FILENAME), + output: { + filename: WEBPACK_ENTRY_FILENAME, + path: outputDir, + }, + devtool: 'source-map', + plugins: [ + new GenerateAssetPlugin({ + filename: 'manifest.js', + fn: TEST_ASSET_CB, + }), + new GenerateAssetPlugin({ + filename: 'manifest.json', + fn: TEST_ASSET_CB, + }), + new InjectManifest({ + swSrc: SW_SRC, + }), + ], + }; + + const compiler = webpack(config); + compiler.run(async (webpackError) => { + if (webpackError) { + return done(webpackError); + } + + const swFile = path.join(outputDir, path.basename(SW_SRC)); + try { + // First, validate that the generated service-worker.js meets some basic assumptions. + await validateServiceWorkerRuntime({swFile, expectedMethodCalls: { + importScripts: [[ + FILE_MANIFEST_NAME, + WORKBOX_SW_FILE_NAME, + ]], + suppressWarnings: [[]], + precacheAndRoute: [[[], {}]], + }}); + + // Next, test the generated manifest to ensure that it contains + // exactly the entries that we expect. + const manifestFileContents = await fse.readFile(path.join(outputDir, FILE_MANIFEST_NAME), 'utf-8'); + const context = {self: {}}; + vm.runInNewContext(manifestFileContents, context); + + const expectedEntries = [{ + revision: '8e8e9f093f036bd18dfa', + url: 'webpackEntry.js', + }]; + expect(context.self.__precacheManifest).to.eql(expectedEntries); + + done(); + } catch (error) { + done(error); + } + }); + }); + + it(`should allow developers to override the default exclude filter`, function(done) { + const FILE_MANIFEST_NAME = 'precache-manifest.b87ef85173e527290f94979b09e72d12.js'; + const outputDir = tempy.directory(); + const config = { + entry: path.join(SRC_DIR, WEBPACK_ENTRY_FILENAME), + output: { + filename: WEBPACK_ENTRY_FILENAME, + path: outputDir, + }, + devtool: 'source-map', + plugins: [ + new InjectManifest({ + exclude: [], + swSrc: SW_SRC, + }), + ], + }; + + const compiler = webpack(config); + compiler.run(async (webpackError) => { + if (webpackError) { + return done(webpackError); + } + + const swFile = path.join(outputDir, path.basename(SW_SRC)); + try { + // First, validate that the generated service-worker.js meets some basic assumptions. + await validateServiceWorkerRuntime({swFile, expectedMethodCalls: { + importScripts: [[ + FILE_MANIFEST_NAME, + WORKBOX_SW_FILE_NAME, + ]], + suppressWarnings: [[]], + precacheAndRoute: [[[], {}]], + }}); + + // Next, test the generated manifest to ensure that it contains + // exactly the entries that we expect. + const manifestFileContents = await fse.readFile(path.join(outputDir, FILE_MANIFEST_NAME), 'utf-8'); + const context = {self: {}}; + vm.runInNewContext(manifestFileContents, context); + + const expectedEntries = [{ + revision: '8e8e9f093f036bd18dfa', + url: 'webpackEntry.js.map', + }, { + revision: '8e8e9f093f036bd18dfa', + url: 'webpackEntry.js', + }]; + expect(context.self.__precacheManifest).to.eql(expectedEntries); + + done(); + } catch (error) { + done(error); + } + }); + }); + + it(`should allow developers to whitelist via include`, function(done) { + const FILE_MANIFEST_NAME = 'precache-manifest.1242f9e007897f035a52e56690ff17a6.js'; + const outputDir = tempy.directory(); + const config = { + entry: path.join(SRC_DIR, WEBPACK_ENTRY_FILENAME), + output: { + filename: WEBPACK_ENTRY_FILENAME, + path: outputDir, + }, + devtool: 'source-map', + plugins: [ + new CopyWebpackPlugin([{ + from: SRC_DIR, + to: outputDir, + }]), + new InjectManifest({ + include: [/.html$/], + swSrc: SW_SRC, + }), + ], + }; + + const compiler = webpack(config); + compiler.run(async (webpackError) => { + if (webpackError) { + return done(webpackError); + } + + const swFile = path.join(outputDir, path.basename(SW_SRC)); + try { + // First, validate that the generated service-worker.js meets some basic assumptions. + await validateServiceWorkerRuntime({swFile, expectedMethodCalls: { + importScripts: [[ + FILE_MANIFEST_NAME, + WORKBOX_SW_FILE_NAME, + ]], + suppressWarnings: [[]], + precacheAndRoute: [[[], {}]], + }}); + + // Next, test the generated manifest to ensure that it contains + // exactly the entries that we expect. + const manifestFileContents = await fse.readFile(path.join(outputDir, FILE_MANIFEST_NAME), 'utf-8'); + const context = {self: {}}; + vm.runInNewContext(manifestFileContents, context); + + const expectedEntries = [{ + revision: 'a3a71ce0b9b43c459cf58bd37e911b74', + url: 'page-2.html', + }, { + revision: '544658ab25ee8762dc241e8b1c5ed96d', + url: 'page-1.html', + }, { + revision: '3883c45b119c9d7e9ad75a1b4a4672ac', + url: 'index.html', + }]; + expect(context.self.__precacheManifest).to.eql(expectedEntries); + + done(); + } catch (error) { + done(error); + } + }); + }); + + it(`should allow developers to combine the test and exclude filters`, function(done) { + const FILE_MANIFEST_NAME = 'precache-manifest.83df65831eb442f8ad5fbeb8edecc558.js'; + const outputDir = tempy.directory(); + const config = { + entry: path.join(SRC_DIR, WEBPACK_ENTRY_FILENAME), + output: { + filename: WEBPACK_ENTRY_FILENAME, + path: outputDir, + }, + devtool: 'source-map', + plugins: [ + new CopyWebpackPlugin([{ + from: SRC_DIR, + to: outputDir, + }]), + new InjectManifest({ + swSrc: SW_SRC, + test: [/.html$/], + exclude: [/index/], + }), + ], + }; + + const compiler = webpack(config); + compiler.run(async (webpackError) => { + if (webpackError) { + return done(webpackError); + } + + const swFile = path.join(outputDir, path.basename(SW_SRC)); + try { + // First, validate that the generated service-worker.js meets some basic assumptions. + await validateServiceWorkerRuntime({swFile, expectedMethodCalls: { + importScripts: [[ + FILE_MANIFEST_NAME, + WORKBOX_SW_FILE_NAME, + ]], + suppressWarnings: [[]], + precacheAndRoute: [[[], {}]], + }}); + + // Next, test the generated manifest to ensure that it contains + // exactly the entries that we expect. + const manifestFileContents = await fse.readFile(path.join(outputDir, FILE_MANIFEST_NAME), 'utf-8'); + const context = {self: {}}; + vm.runInNewContext(manifestFileContents, context); + + const expectedEntries = [{ + revision: 'a3a71ce0b9b43c459cf58bd37e911b74', + url: 'page-2.html', + }, { + revision: '544658ab25ee8762dc241e8b1c5ed96d', + url: 'page-1.html', + }]; + expect(context.self.__precacheManifest).to.eql(expectedEntries); + + done(); + } catch (error) { + done(error); + } + }); + }); + }); });