diff --git a/config/auto-sw-plugin.js b/config/auto-sw-plugin.js new file mode 100644 index 00000000..d00c5ea1 --- /dev/null +++ b/config/auto-sw-plugin.js @@ -0,0 +1,156 @@ +const util = require('util'); +const minimatch = require('minimatch'); +const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin'); +const WebWorkerTemplatePlugin = require('webpack/lib/webworker/WebWorkerTemplatePlugin'); +const ParserHelpers = require('webpack/lib/ParserHelpers'); + +const NAME = 'auto-sw-plugin'; +const JS_TYPES = ['auto', 'esm', 'dynamic']; + +/** + * Automatically finds and bundles Service Workers by looking for navigator.serviceWorker.register(..). + * An Array of webpack assets is injected into the Service Worker bundle as a `BUILD_ASSETS` global. + * Hidden and `.map` files are excluded by default, and this can be customized using the include & exclude options. + * @example + * // webpack config + * plugins: [ + * new AutoSWPlugin({ + * exclude: [ + * '**\/.*', // don't expose hidden files (default) + * '**\/*.map', // don't precache sourcemaps (default) + * 'index.html' // don't cache the page itself + * ] + * }) + * ] + * @param {Object} [options={}] + * @param {string[]} [options.exclude] Minimatch pattern(s) of which assets to omit from BUILD_ASSETS. + * @param {string[]} [options.include] Minimatch pattern(s) of assets to allow in BUILD_ASSETS. + */ +module.exports = class AutoSWPlugin { + constructor(options) { + this.options = Object.assign({ + exclude: [ + '**/*.map', + '**/.*' + ] + }, options || {}); + } + + apply(compiler) { + const serviceWorkers = []; + + compiler.hooks.emit.tapPromise(NAME, compilation => this.emit(compiler, compilation, serviceWorkers)); + + compiler.hooks.normalModuleFactory.tap(NAME, (factory) => { + for (const type of JS_TYPES) { + factory.hooks.parser.for(`javascript/${type}`).tap(NAME, parser => { + let counter = 0; + + const processRegisterCall = expr => { + const dep = parser.evaluateExpression(expr.arguments[0]); + + if (!dep.isString()) { + parser.state.module.warnings.push({ + message: 'navigator.serviceWorker.register() will only be bundled if passed a String literal.' + }); + return false; + } + + const filename = dep.string; + const outputFilename = this.options.filename || 'serviceworker.js' + const context = parser.state.current.context; + serviceWorkers.push({ + outputFilename, + filename, + context + }); + + const id = `__webpack__serviceworker__${++counter}`; + ParserHelpers.toConstantDependency(parser, id)(expr.arguments[0]); + return ParserHelpers.addParsedVariableToModule(parser, id, '__webpack_public_path__ + ' + JSON.stringify(outputFilename)); + }; + + parser.hooks.call.for('navigator.serviceWorker.register').tap(NAME, processRegisterCall); + parser.hooks.call.for('self.navigator.serviceWorker.register').tap(NAME, processRegisterCall); + parser.hooks.call.for('window.navigator.serviceWorker.register').tap(NAME, processRegisterCall); + }); + } + }); + } + + createFilter(list) { + const filters = [].concat(list); + for (let i=0; i { + for (const filter of filters) { + if (filter(filename)) return true; + } + return false; + }); + } + if (this.options.exclude) { + const filters = this.createFilter(this.options.exclude); + assetMapping = assetMapping.filter(filename => { + for (const filter of filters) { + if (filter(filename)) return false; + } + return true; + }); + } + await Promise.all(serviceWorkers.map( + (serviceWorker, index) => this.compileServiceWorker(compiler, compilation, serviceWorker, index, assetMapping) + )); + } + + async compileServiceWorker(compiler, compilation, options, index, assetMapping) { + const entryFilename = options.filename; + + const chunkFilename = compiler.options.output.chunkFilename.replace(/\.([a-z]+)$/i, '.serviceworker.$1'); + const workerOptions = { + filename: options.outputFilename, // chunkFilename.replace(/\.?\[(?:chunkhash|contenthash|hash)(:\d+(?::\d+)?)?\]/g, ''), + chunkFilename: this.options.chunkFilename || chunkFilename, + globalObject: 'self' + }; + + const childCompiler = compilation.createChildCompiler(NAME, { filename: workerOptions.filename }); + (new WebWorkerTemplatePlugin(workerOptions)).apply(childCompiler); + + /* The duplication DefinePlugin ends up causing is problematic (it doesn't hoist injections), so we'll do it manually. */ + // (new DefinePlugin({ + // BUILD_ASSETS: JSON.stringify(assetMapping) + // })).apply(childCompiler); + (new SingleEntryPlugin(options.context, entryFilename, workerOptions.filename)).apply(childCompiler); + + const subCache = `subcache ${__dirname} ${entryFilename} ${index}`; + let childCompilation; + childCompiler.hooks.compilation.tap(NAME, c => { + childCompilation = c; + if (childCompilation.cache) { + if (!childCompilation.cache[subCache]) childCompilation.cache[subCache] = {}; + childCompilation.cache = childCompilation.cache[subCache]; + } + }); + + await (util.promisify(childCompiler.runAsChild.bind(childCompiler)))(); + + const original = childCompilation.assets[workerOptions.filename].source(); + const source = `var BUILD_ASSETS=${JSON.stringify(assetMapping)};\n${original}`; + childCompilation.assets[workerOptions.filename] = { + source: () => source, + size: () => Buffer.byteLength(source, 'utf8') + }; + + Object.assign(compilation.assets, childCompilation.assets); + } +}; diff --git a/package-lock.json b/package-lock.json index dc2848d5..59059e86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5553,6 +5553,12 @@ } } }, + "idb-keyval": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-3.1.0.tgz", + "integrity": "sha512-iFwFN5n00KNNnVxlOOK280SJJfXWY7pbMUOQXdIXehvvc/mGCV/6T2Ae+Pk2KwAkkATDTwfMavOiDH5lrJKWXQ==", + "dev": true + }, "ieee754": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", diff --git a/package.json b/package.json index 09af2400..c6bb856d 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "file-loader": "^1.1.11", "html-webpack-plugin": "^3.2.0", "husky": "^1.1.2", + "idb-keyval": "^3.1.0", "if-env": "^1.0.4", "linkstate": "^1.1.1", "loader-utils": "^1.1.0", @@ -47,6 +48,7 @@ "node-sass": "^4.9.4", "optimize-css-assets-webpack-plugin": "^4.0.3", "pointer-tracker": "^2.0.3", + "minimatch": "^3.0.4", "preact": "^8.3.1", "pretty-bytes": "^5.1.0", "progress-bar-webpack-plugin": "^1.11.0", @@ -62,7 +64,8 @@ "tslint-react": "^3.6.0", "typescript": "^2.9.2", "typings-for-css-modules-loader": "^1.7.0", - "webpack": "=4.19.1", + "url-loader": "^1.1.2", + "webpack": "^4.19.1", "webpack-bundle-analyzer": "^2.13.1", "webpack-cli": "^2.1.5", "webpack-dev-server": "^3.1.5", diff --git a/src/codecs/decoders.ts b/src/codecs/decoders.ts index 6ffb09c2..8347bcf2 100644 --- a/src/codecs/decoders.ts +++ b/src/codecs/decoders.ts @@ -1,9 +1,8 @@ import { nativeDecode, sniffMimeType, canDecodeImage } from '../lib/util'; import Processor from './processor'; +import webpDataUrl from 'url-loader!./tiny.webp'; -// tslint:disable-next-line:max-line-length It’s a data URL. Whatcha gonna do? -const webpFile = 'data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA='; -const nativeWebPSupported = canDecodeImage(webpFile); +const nativeWebPSupported = canDecodeImage(webpDataUrl); export async function decodeImage(blob: Blob, processor: Processor): Promise { const mimeType = await sniffMimeType(blob); diff --git a/src/codecs/processor-worker.ts b/src/codecs/processor-worker.ts index 7e6b110e..d2a50019 100644 --- a/src/codecs/processor-worker.ts +++ b/src/codecs/processor-worker.ts @@ -7,31 +7,46 @@ import { EncodeOptions as WebPEncoderOptions } from './webp/encoder-meta'; async function mozjpegEncode( data: ImageData, options: MozJPEGEncoderOptions, ): Promise { - const { encode } = await import('./mozjpeg/encoder'); + const { encode } = await import( + /* webpackChunkName: "process-mozjpeg-enc" */ + './mozjpeg/encoder', + ); return encode(data, options); } async function quantize(data: ImageData, opts: QuantizeOptions): Promise { - const { process } = await import('./imagequant/processor'); + const { process } = await import( + /* webpackChunkName: "process-imagequant" */ + './imagequant/processor', + ); return process(data, opts); } async function optiPngEncode( data: BufferSource, options: OptiPNGEncoderOptions, ): Promise { - const { compress } = await import('./optipng/encoder'); + const { compress } = await import( + /* webpackChunkName: "process-optipng" */ + './optipng/encoder', + ); return compress(data, options); } async function webpEncode( data: ImageData, options: WebPEncoderOptions, ): Promise { - const { encode } = await import('./webp/encoder'); + const { encode } = await import( + /* webpackChunkName: "process-webp-enc" */ + './webp/encoder', + ); return encode(data, options); } async function webpDecode(data: ArrayBuffer): Promise { - const { decode } = await import('./webp/decoder'); + const { decode } = await import( + /* webpackChunkName: "process-webp-dec" */ + './webp/decoder', + ); return decode(data); } diff --git a/src/codecs/processor.ts b/src/codecs/processor.ts index 93e0e6b5..499af8a6 100644 --- a/src/codecs/processor.ts +++ b/src/codecs/processor.ts @@ -61,7 +61,10 @@ export default class Processor { // worker-loader does magic here. // @ts-ignore - Typescript doesn't know about the 2nd param to new Worker, and the // definition can't be overwritten. - this._worker = new Worker('./processor-worker.ts', { type: 'module' }) as Worker; + this._worker = new Worker( + './processor-worker.ts', + { name: 'processor-worker', type: 'module' }, + ) as Worker; // Need to do some TypeScript trickery to make the type match. this._workerApi = proxy(this._worker) as any as ProcessorWorkerApi; } diff --git a/src/codecs/tiny.webp b/src/codecs/tiny.webp new file mode 100644 index 00000000..9652d275 Binary files /dev/null and b/src/codecs/tiny.webp differ diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 724fbd22..4c7889cd 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -36,12 +36,17 @@ export default class App extends Component { constructor() { super(); - import('../compress').then((module) => { + import( + /* webpackChunkName: "main-app" */ + '../compress', + ).then((module) => { this.setState({ Compress: module.default }); }).catch(() => { this.showSnack('Failed to load app'); }); + navigator.serviceWorker.register('../../sw'); + // In development, persist application state across hot reloads: if (process.env.NODE_ENV === 'development') { this.setState(window.STATE); diff --git a/src/components/compress/index.tsx b/src/components/compress/index.tsx index ef5f19aa..6048e324 100644 --- a/src/components/compress/index.tsx +++ b/src/components/compress/index.tsx @@ -1,4 +1,5 @@ import { h, Component } from 'preact'; +import { get, set } from 'idb-keyval'; import { bind, Fileish } from '../../lib/initial-util'; import { blobToImg, drawableToImageData, blobToText } from '../../lib/util'; @@ -155,6 +156,12 @@ async function processSvg(blob: Blob): Promise { return blobToImg(new Blob([newSource], { type: 'image/svg+xml' })); } +async function getOldestServiceWorker() { + const reg = await navigator.serviceWorker.getRegistration(); + if (!reg) return null; + return reg.active || reg.waiting || reg.installing; +} + // These are only used in the mobile view const resultTitles = ['Top', 'Bottom']; // These are only used in the desktop view @@ -195,6 +202,17 @@ export default class Compress extends Component { super(props); this.widthQuery.addListener(this.onMobileWidthChange); this.updateFile(props.file); + + // If this is the first time the user has interacted with the app, tell the service worker to + // cache all the codecs. + get('user-interacted') + .then(async (userInteracted: boolean | undefined) => { + if (userInteracted) return; + set('user-interacted', true); + const serviceWorker = await getOldestServiceWorker(); + if (!serviceWorker) return; // Service worker not installing yet. + serviceWorker.postMessage('cache-all'); + }); } @bind diff --git a/src/components/intro/imgs/demos/artwork.jpg b/src/components/intro/imgs/demos/demo-artwork.jpg similarity index 100% rename from src/components/intro/imgs/demos/artwork.jpg rename to src/components/intro/imgs/demos/demo-artwork.jpg diff --git a/src/components/intro/imgs/demos/device-screen.png b/src/components/intro/imgs/demos/demo-device-screen.png similarity index 100% rename from src/components/intro/imgs/demos/device-screen.png rename to src/components/intro/imgs/demos/demo-device-screen.png diff --git a/src/components/intro/imgs/demos/large-photo.jpg b/src/components/intro/imgs/demos/demo-large-photo.jpg similarity index 100% rename from src/components/intro/imgs/demos/large-photo.jpg rename to src/components/intro/imgs/demos/demo-large-photo.jpg diff --git a/src/components/intro/imgs/demos/artwork-icon.jpg b/src/components/intro/imgs/demos/icon-demo-artwork.jpg similarity index 100% rename from src/components/intro/imgs/demos/artwork-icon.jpg rename to src/components/intro/imgs/demos/icon-demo-artwork.jpg diff --git a/src/components/intro/imgs/demos/device-screen-icon.jpg b/src/components/intro/imgs/demos/icon-demo-device-screen.jpg similarity index 100% rename from src/components/intro/imgs/demos/device-screen-icon.jpg rename to src/components/intro/imgs/demos/icon-demo-device-screen.jpg diff --git a/src/components/intro/imgs/demos/large-photo-icon.jpg b/src/components/intro/imgs/demos/icon-demo-large-photo.jpg similarity index 100% rename from src/components/intro/imgs/demos/large-photo-icon.jpg rename to src/components/intro/imgs/demos/icon-demo-large-photo.jpg diff --git a/src/components/intro/imgs/demos/logo-icon.png b/src/components/intro/imgs/demos/icon-demo-logo.png similarity index 100% rename from src/components/intro/imgs/demos/logo-icon.png rename to src/components/intro/imgs/demos/icon-demo-logo.png diff --git a/src/components/intro/index.tsx b/src/components/intro/index.tsx index 89dbb03a..cdda264d 100644 --- a/src/components/intro/index.tsx +++ b/src/components/intro/index.tsx @@ -4,13 +4,13 @@ import { bind, linkRef, Fileish } from '../../lib/initial-util'; import '../custom-els/LoadingSpinner'; import logo from './imgs/logo.svg'; -import largePhoto from './imgs/demos/large-photo.jpg'; -import artwork from './imgs/demos/artwork.jpg'; -import deviceScreen from './imgs/demos/device-screen.png'; -import largePhotoIcon from './imgs/demos/large-photo-icon.jpg'; -import artworkIcon from './imgs/demos/artwork-icon.jpg'; -import deviceScreenIcon from './imgs/demos/device-screen-icon.jpg'; -import logoIcon from './imgs/demos/logo-icon.png'; +import largePhoto from './imgs/demos/demo-large-photo.jpg'; +import artwork from './imgs/demos/demo-artwork.jpg'; +import deviceScreen from './imgs/demos/demo-device-screen.png'; +import largePhotoIcon from './imgs/demos/icon-demo-large-photo.jpg'; +import artworkIcon from './imgs/demos/icon-demo-artwork.jpg'; +import deviceScreenIcon from './imgs/demos/icon-demo-device-screen.jpg'; +import logoIcon from './imgs/demos/icon-demo-logo.png'; import * as style from './style.scss'; import SnackBarElement from '../../lib/SnackBar'; diff --git a/src/index.html b/src/index.html index 6741b9ed..77d7147a 100644 --- a/src/index.html +++ b/src/index.html @@ -8,6 +8,7 @@ + diff --git a/src/index.ts b/src/index.ts index 5e9d5c59..b88db26b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,14 @@ declare module '@webcomponents/custom-elements'; -(async function () { - if (!('customElements' in self)) { - await import('@webcomponents/custom-elements'); - } - +function init() { require('./init-app.tsx'); -})(); +} + +if (!('customElements' in self)) { + import( + /* webpackChunkName: "wc-polyfill" */ + '@webcomponents/custom-elements', + ).then(init); +} else { + init(); +} diff --git a/src/init-app.tsx b/src/init-app.tsx index f0102af0..8d5cabe9 100644 --- a/src/init-app.tsx +++ b/src/init-app.tsx @@ -9,8 +9,7 @@ let root = document.querySelector('#app') || undefined; // "attach" the client-side rendering to it, updating the DOM in-place instead of replacing: root = render(, document.body, root); -// In production, this entire condition is removed. -if (process.env.NODE_ENV === 'development') { +if (process.env.NODE_ENV !== 'production') { // Enable support for React DevTools and some helpful console warnings: require('preact/debug'); diff --git a/src/manifest.json b/src/manifest.json index 144b8322..81ad2a44 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -8,9 +8,9 @@ "theme_color": "#673ab8", "icons": [ { - "src": "/assets/icon.png", + "src": "/assets/icon-large.png", "type": "image/png", - "sizes": "512x512" + "sizes": "1024x1024" } ] -} \ No newline at end of file +} diff --git a/src/missing-types.d.ts b/src/missing-types.d.ts index cbc5021a..491ab790 100644 --- a/src/missing-types.d.ts +++ b/src/missing-types.d.ts @@ -27,3 +27,8 @@ declare module '*.wasm' { const content: string; export default content; } + +declare module 'url-loader!*' { + const value: string; + export default value; +} diff --git a/src/sw/index.ts b/src/sw/index.ts new file mode 100644 index 00000000..310d2c84 --- /dev/null +++ b/src/sw/index.ts @@ -0,0 +1,63 @@ +import { + cacheOrNetworkAndCache, cleanupCache, cacheOrNetwork, cacheBasics, cacheAdditionalProcessors, +} from './util'; +import { get } from 'idb-keyval'; + +// Give TypeScript the correct global. +declare var self: ServiceWorkerGlobalScope; +// This is populated by webpack. +declare var BUILD_ASSETS: string[]; + +const version = '1.0.0'; +const versionedCache = 'static-' + version; +const dynamicCache = 'dynamic'; +const expectedCaches = [versionedCache, dynamicCache]; + +self.addEventListener('install', (event) => { + event.waitUntil(async function () { + const promises = []; + promises.push(cacheBasics(versionedCache, BUILD_ASSETS)); + + // If the user has already interacted with the app, update the codecs too. + if (await get('user-interacted')) { + promises.push(cacheAdditionalProcessors(versionedCache, BUILD_ASSETS)); + } + + await Promise.all(promises); + }()); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil(async function () { + // Remove old caches. + const promises = (await caches.keys()).map((cacheName) => { + if (!expectedCaches.includes(cacheName)) return caches.delete(cacheName); + }); + + await Promise.all(promises); + }()); +}); + +self.addEventListener('fetch', (event) => { + // We only care about GET. + if (event.request.method !== 'GET') return; + + const url = new URL(event.request.url); + + // Don't care about other-origin URLs + if (url.origin !== location.origin) return; + + if (url.pathname.startsWith('/demo-') || url.pathname.startsWith('/wc-polyfill')) { + cacheOrNetworkAndCache(event, dynamicCache); + cleanupCache(event, dynamicCache, BUILD_ASSETS); + return; + } + + cacheOrNetwork(event); +}); + +self.addEventListener('message', (event) => { + if (event.data === 'cache-all') { + event.waitUntil(cacheAdditionalProcessors(versionedCache, BUILD_ASSETS)); + } +}); diff --git a/src/sw/missing-types.d.ts b/src/sw/missing-types.d.ts new file mode 100644 index 00000000..ec041d77 --- /dev/null +++ b/src/sw/missing-types.d.ts @@ -0,0 +1 @@ +import '../missing-types'; diff --git a/src/sw/tsconfig.json b/src/sw/tsconfig.json new file mode 100644 index 00000000..efb9ce2b --- /dev/null +++ b/src/sw/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "target": "esnext", + "module": "esnext", + "lib": [ + "webworker", + "esnext" + ], + "moduleResolution": "node", + "experimentalDecorators": true, + "noUnusedLocals": true, + "sourceMap": true, + "allowJs": false, + "baseUrl": "." + } +} diff --git a/src/sw/util.ts b/src/sw/util.ts new file mode 100644 index 00000000..fd5582a9 --- /dev/null +++ b/src/sw/util.ts @@ -0,0 +1,105 @@ +import webpDataUrl from 'url-loader!../codecs/tiny.webp'; + +export function cacheOrNetwork(event: FetchEvent): void { + event.respondWith(async function () { + const cachedResponse = await caches.match(event.request); + return cachedResponse || fetch(event.request); + }()); +} + +export function cacheOrNetworkAndCache(event: FetchEvent, cacheName: string): void { + event.respondWith(async function () { + const { request } = event; + // Return from cache if possible. + const cachedResponse = await caches.match(request); + if (cachedResponse) return cachedResponse; + + // Else go to the network. + const response = await fetch(request); + const responseToCache = response.clone(); + + event.waitUntil(async function () { + // Cache what we fetched. + const cache = await caches.open(cacheName); + await cache.put(request, responseToCache); + }()); + + // Return the network response. + return response; + }()); +} + +export function cleanupCache(event: FetchEvent, cacheName: string, keepAssets: string[]) { + event.waitUntil(async function () { + const cache = await caches.open(cacheName); + + // Clean old entries from the dynamic cache. + const requests = await cache.keys(); + const promises = requests.map((cachedRequest) => { + // Get pathname without leading / + const assetPath = new URL(cachedRequest.url).pathname.slice(1); + // If it isn't one of our keepAssets, we don't need it anymore. + if (!keepAssets.includes(assetPath)) return cache.delete(cachedRequest); + }); + + await Promise.all(promises); + }()); +} + +export async function cacheBasics(cacheName: string, buildAssets: string[]) { + const toCache = ['/', '/assets/favicon.ico']; + + const prefixesToCache = [ + // First interaction JS & CSS: + 'first-interaction.', + // Main app JS & CSS: + 'main-app.', + // Little icons for the demo images on the homescreen: + 'icon-demo-', + // Site logo: + 'logo.', + ]; + + const prefixMatches = buildAssets.filter( + asset => prefixesToCache.some(prefix => asset.startsWith(prefix)), + ); + + toCache.push(...prefixMatches); + + const cache = await caches.open(cacheName); + await cache.addAll(toCache); +} + +export async function cacheAdditionalProcessors(cacheName: string, buildAssets: string[]) { + let toCache = []; + + const prefixesToCache = [ + // Worker which handles image processing: + 'processor-worker.', + // processor-worker imports: + 'process-', + ]; + + const prefixMatches = buildAssets.filter( + asset => prefixesToCache.some(prefix => asset.startsWith(prefix)), + ); + + const wasm = buildAssets.filter(asset => asset.endsWith('.wasm')); + + toCache.push(...prefixMatches, ...wasm); + + const supportsWebP = await (async () => { + if (!self.createImageBitmap) return false; + const response = await fetch(webpDataUrl); + const blob = await response.blob(); + return createImageBitmap(blob).then(() => true, () => false); + })(); + + // No point caching the WebP decoder if it's supported natively: + if (supportsWebP) { + toCache = toCache.filter(asset => !/webp[\-_]dec/.test(asset)); + } + + const cache = await caches.open(cacheName); + await cache.addAll(toCache); +} diff --git a/tsconfig.json b/tsconfig.json index 8dbfb6a8..835a1240 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,6 @@ "jsxFactory": "h", "allowJs": false, "baseUrl": "." - } -} \ No newline at end of file + }, + "exclude": ["src/sw/**/*"] +} diff --git a/webpack.config.js b/webpack.config.js index 6b753b9f..5e164d01 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,6 +13,7 @@ const CopyPlugin = require('copy-webpack-plugin'); const WatchTimestampsPlugin = require('./config/watch-timestamps-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const WorkerPlugin = require('worker-plugin'); +const AutoSWPlugin = require('./config/auto-sw-plugin'); function readJson (filename) { return JSON.parse(fs.readFileSync(filename)); @@ -30,12 +31,14 @@ module.exports = function (_, env) { return { mode: isProd ? 'production' : 'development', - entry: './src/index', + entry: { + 'first-interaction': './src/index' + }, devtool: isProd ? 'source-map' : 'inline-source-map', stats: 'minimal', output: { filename: isProd ? '[name].[chunkhash:5].js' : '[name].js', - chunkFilename: '[name].chunk.[chunkhash:5].js', + chunkFilename: '[name].[chunkhash:5].js', path: path.join(__dirname, 'build'), publicPath: '/', globalObject: 'self' @@ -154,11 +157,17 @@ module.exports = function (_, env) { // This is needed to make webpack NOT process wasm files. // See https://github.com/webpack/webpack/issues/6725 type: 'javascript/auto', - loader: 'file-loader' + loader: 'file-loader', + options: { + name: '[name].[hash:5].[ext]', + }, }, { test: /\.(png|svg|jpg|gif)$/, - loader: 'file-loader' + loader: 'file-loader', + options: { + name: '[name].[hash:5].[ext]', + }, } ] }, @@ -195,7 +204,7 @@ module.exports = function (_, env) { // See also: https://twitter.com/wsokra/status/970253245733113856 isProd && new MiniCssExtractPlugin({ filename: '[name].[contenthash:5].css', - chunkFilename: '[name].chunk.[contenthash:5].css' + chunkFilename: '[name].[contenthash:5].css' }), new OptimizeCssAssetsPlugin({ @@ -233,6 +242,8 @@ module.exports = function (_, env) { compile: true }), + new AutoSWPlugin({}), + new ScriptExtHtmlPlugin({ defaultAttribute: 'async' }),