mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-12 08:47:31 +00:00
Enhanced offline (#249)
* Notification of updates & reloading * Using version in service worker & allowing version to appear elsewhere * Stupid file * Ditching changelog for now. Using package json. * Ugh.
This commit is contained in:
@@ -144,8 +144,10 @@ module.exports = class AutoSWPlugin {
|
|||||||
|
|
||||||
await (util.promisify(childCompiler.runAsChild.bind(childCompiler)))();
|
await (util.promisify(childCompiler.runAsChild.bind(childCompiler)))();
|
||||||
|
|
||||||
|
const versionVar = this.options.version ?
|
||||||
|
`var VERSION = ${JSON.stringify(this.options.version)};` : '';
|
||||||
const original = childCompilation.assets[workerOptions.filename].source();
|
const original = childCompilation.assets[workerOptions.filename].source();
|
||||||
const source = `var BUILD_ASSETS=${JSON.stringify(assetMapping)};\n${original}`;
|
const source = `${versionVar}var BUILD_ASSETS=${JSON.stringify(assetMapping)};${original}`;
|
||||||
childCompilation.assets[workerOptions.filename] = {
|
childCompilation.assets[workerOptions.filename] = {
|
||||||
source: () => source,
|
source: () => source,
|
||||||
size: () => Buffer.byteLength(source, 'utf8')
|
size: () => Buffer.byteLength(source, 'utf8')
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "squoosh",
|
"name": "squoosh",
|
||||||
"version": "0.0.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -14245,12 +14245,6 @@
|
|||||||
"uuid": "^3.3.2"
|
"uuid": "^3.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"webpack-plugin-replace": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/webpack-plugin-replace/-/webpack-plugin-replace-1.1.1.tgz",
|
|
||||||
"integrity": "sha1-mBZkf+/Jin0XAPk/K0tvSBzyAWE=",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"webpack-sources": {
|
"webpack-sources": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.2.0.tgz",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "squoosh",
|
"name": "squoosh",
|
||||||
"version": "0.0.0",
|
"version": "0.1.0",
|
||||||
"license": "apache-2.0",
|
"license": "apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack serve --host 0.0.0.0 --hot",
|
"start": "webpack serve --host 0.0.0.0 --hot",
|
||||||
@@ -71,7 +71,6 @@
|
|||||||
"webpack-bundle-analyzer": "^2.13.1",
|
"webpack-bundle-analyzer": "^2.13.1",
|
||||||
"webpack-cli": "^2.1.5",
|
"webpack-cli": "^2.1.5",
|
||||||
"webpack-dev-server": "^3.1.5",
|
"webpack-dev-server": "^3.1.5",
|
||||||
"webpack-plugin-replace": "^1.1.1",
|
|
||||||
"worker-plugin": "^1.1.1"
|
"worker-plugin": "^1.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ import '../custom-els/LoadingSpinner';
|
|||||||
// This is imported for TypeScript only. It isn't used.
|
// This is imported for TypeScript only. It isn't used.
|
||||||
import Compress from '../compress';
|
import Compress from '../compress';
|
||||||
|
|
||||||
|
const compressPromise = import(
|
||||||
|
/* webpackChunkName: "main-app" */
|
||||||
|
'../compress',
|
||||||
|
);
|
||||||
|
const offlinerPromise = import(
|
||||||
|
/* webpackChunkName: "offliner" */
|
||||||
|
'../../lib/offliner',
|
||||||
|
);
|
||||||
|
|
||||||
export interface SourceImage {
|
export interface SourceImage {
|
||||||
file: File | Fileish;
|
file: File | Fileish;
|
||||||
data: ImageData;
|
data: ImageData;
|
||||||
@@ -36,16 +45,13 @@ export default class App extends Component<Props, State> {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
import(
|
compressPromise.then((module) => {
|
||||||
/* webpackChunkName: "main-app" */
|
|
||||||
'../compress',
|
|
||||||
).then((module) => {
|
|
||||||
this.setState({ Compress: module.default });
|
this.setState({ Compress: module.default });
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.showSnack('Failed to load app');
|
this.showSnack('Failed to load app');
|
||||||
});
|
});
|
||||||
|
|
||||||
navigator.serviceWorker.register('../../sw');
|
offlinerPromise.then(({ offliner }) => offliner(this.showSnack));
|
||||||
|
|
||||||
// In development, persist application state across hot reloads:
|
// In development, persist application state across hot reloads:
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
import { get, set } from 'idb-keyval';
|
|
||||||
|
|
||||||
import { bind, Fileish } from '../../lib/initial-util';
|
import { bind, Fileish } from '../../lib/initial-util';
|
||||||
import { blobToImg, drawableToImageData, blobToText } from '../../lib/util';
|
import { blobToImg, drawableToImageData, blobToText } from '../../lib/util';
|
||||||
@@ -156,12 +155,6 @@ async function processSvg(blob: Blob): Promise<HTMLImageElement> {
|
|||||||
return blobToImg(new Blob([newSource], { type: 'image/svg+xml' }));
|
return blobToImg(new Blob([newSource], { type: 'image/svg+xml' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMostActiveServiceWorker() {
|
|
||||||
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
|
// These are only used in the mobile view
|
||||||
const resultTitles = ['Top', 'Bottom'];
|
const resultTitles = ['Top', 'Bottom'];
|
||||||
// These are only used in the desktop view
|
// These are only used in the desktop view
|
||||||
@@ -203,16 +196,7 @@ export default class Compress extends Component<Props, State> {
|
|||||||
this.widthQuery.addListener(this.onMobileWidthChange);
|
this.widthQuery.addListener(this.onMobileWidthChange);
|
||||||
this.updateFile(props.file);
|
this.updateFile(props.file);
|
||||||
|
|
||||||
// If this is the first time the user has interacted with the app, tell the service worker to
|
import('../../lib/offliner').then(({ mainAppLoaded }) => mainAppLoaded());
|
||||||
// cache all the codecs.
|
|
||||||
get<boolean | undefined>('user-interacted')
|
|
||||||
.then(async (userInteracted: boolean | undefined) => {
|
|
||||||
if (userInteracted) return;
|
|
||||||
set('user-interacted', true);
|
|
||||||
const serviceWorker = await getMostActiveServiceWorker();
|
|
||||||
if (!serviceWorker) return; // Service worker not installing yet.
|
|
||||||
serviceWorker.postMessage('cache-all');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
|
|||||||
@@ -8,12 +8,9 @@ export interface SnackOptions {
|
|||||||
function createSnack(message: string, options: SnackOptions): [Element, Promise<string>] {
|
function createSnack(message: string, options: SnackOptions): [Element, Promise<string>] {
|
||||||
const {
|
const {
|
||||||
timeout = 0,
|
timeout = 0,
|
||||||
actions = [],
|
actions = ['dismiss'],
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// Provide a default 'dismiss' action
|
|
||||||
if (!timeout && actions.length === 0) actions.push('dismiss');
|
|
||||||
|
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = style.snackbar;
|
el.className = style.snackbar;
|
||||||
el.setAttribute('aria-live', 'assertive');
|
el.setAttribute('aria-live', 'assertive');
|
||||||
|
|||||||
91
src/lib/offliner.ts
Normal file
91
src/lib/offliner.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { get, set } from 'idb-keyval';
|
||||||
|
|
||||||
|
// Just for TypeScript
|
||||||
|
import SnackBarElement from './SnackBar';
|
||||||
|
|
||||||
|
/** Tell the service worker to skip waiting */
|
||||||
|
async function skipWaiting() {
|
||||||
|
const reg = await navigator.serviceWorker.getRegistration();
|
||||||
|
if (!reg || !reg.waiting) return;
|
||||||
|
reg.waiting.postMessage('skip-waiting');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find the service worker that's 'active' or closest to 'active' */
|
||||||
|
async function getMostActiveServiceWorker() {
|
||||||
|
const reg = await navigator.serviceWorker.getRegistration();
|
||||||
|
if (!reg) return null;
|
||||||
|
return reg.active || reg.waiting || reg.installing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wait for an installing worker */
|
||||||
|
async function installingWorker(reg: ServiceWorkerRegistration): Promise<ServiceWorker> {
|
||||||
|
if (reg.installing) return reg.installing;
|
||||||
|
return new Promise<ServiceWorker>((resolve) => {
|
||||||
|
reg.addEventListener(
|
||||||
|
'updatefound',
|
||||||
|
() => resolve(reg.installing!),
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wait a service worker to become waiting */
|
||||||
|
async function updateReady(reg: ServiceWorkerRegistration): Promise<void> {
|
||||||
|
if (reg.waiting) return;
|
||||||
|
const installing = await installingWorker(reg);
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
installing.addEventListener('statechange', () => {
|
||||||
|
if (installing.state === 'installed') resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set up the service worker and monitor changes */
|
||||||
|
export async function offliner(showSnack: SnackBarElement['showSnackbar']) {
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
navigator.serviceWorker.register('../sw');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasController = !!navigator.serviceWorker.controller;
|
||||||
|
|
||||||
|
// Look for changes in the controller
|
||||||
|
navigator.serviceWorker.addEventListener('controllerchange', async () => {
|
||||||
|
// Is it the first install?
|
||||||
|
if (!hasController) {
|
||||||
|
showSnack('Ready to work offline', { timeout: 5000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise reload (the user will have agreed to this).
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
const reg = await navigator.serviceWorker.getRegistration();
|
||||||
|
// Service worker not registered yet.
|
||||||
|
if (!reg) return;
|
||||||
|
// Look for updates
|
||||||
|
await updateReady(reg);
|
||||||
|
|
||||||
|
// Ask the user if they want to update.
|
||||||
|
const result = await showSnack('Update available', {
|
||||||
|
actions: ['reload', 'dismiss'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tell the waiting worker to activate, this will change the controller and cause a reload (see
|
||||||
|
// 'controllerchange')
|
||||||
|
if (result === 'reload') skipWaiting();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tell the service worker the main app has loaded. If it's the first time the service worker has
|
||||||
|
* heard about this, cache the heavier assets like codecs.
|
||||||
|
*/
|
||||||
|
export async function mainAppLoaded() {
|
||||||
|
// If the user has already interacted, no need to tell the service worker anything.
|
||||||
|
const userInteracted = await get<boolean | undefined>('user-interacted');
|
||||||
|
if (userInteracted) return;
|
||||||
|
set('user-interacted', true);
|
||||||
|
const serviceWorker = await getMostActiveServiceWorker();
|
||||||
|
if (!serviceWorker) return; // Service worker not installing yet.
|
||||||
|
serviceWorker.postMessage('cache-all');
|
||||||
|
}
|
||||||
2
src/missing-types.d.ts
vendored
2
src/missing-types.d.ts
vendored
@@ -32,3 +32,5 @@ declare module 'url-loader!*' {
|
|||||||
const value: string;
|
const value: string;
|
||||||
export default value;
|
export default value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare var VERSION: string;
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ declare var self: ServiceWorkerGlobalScope;
|
|||||||
// This is populated by webpack.
|
// This is populated by webpack.
|
||||||
declare var BUILD_ASSETS: string[];
|
declare var BUILD_ASSETS: string[];
|
||||||
|
|
||||||
const version = '1.0.0';
|
const versionedCache = 'static-' + VERSION;
|
||||||
const versionedCache = 'static-' + version;
|
|
||||||
const dynamicCache = 'dynamic';
|
const dynamicCache = 'dynamic';
|
||||||
const expectedCaches = [versionedCache, dynamicCache];
|
const expectedCaches = [versionedCache, dynamicCache];
|
||||||
|
|
||||||
@@ -28,6 +27,8 @@ self.addEventListener('install', (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('activate', (event) => {
|
self.addEventListener('activate', (event) => {
|
||||||
|
self.clients.claim();
|
||||||
|
|
||||||
event.waitUntil(async function () {
|
event.waitUntil(async function () {
|
||||||
// Remove old caches.
|
// Remove old caches.
|
||||||
const promises = (await caches.keys()).map((cacheName) => {
|
const promises = (await caches.keys()).map((cacheName) => {
|
||||||
@@ -57,7 +58,12 @@ self.addEventListener('fetch', (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('message', (event) => {
|
self.addEventListener('message', (event) => {
|
||||||
if (event.data === 'cache-all') {
|
switch (event.data) {
|
||||||
event.waitUntil(cacheAdditionalProcessors(versionedCache, BUILD_ASSETS));
|
case 'cache-all':
|
||||||
|
event.waitUntil(cacheAdditionalProcessors(versionedCache, BUILD_ASSETS));
|
||||||
|
break;
|
||||||
|
case 'skip-waiting':
|
||||||
|
self.skipWaiting();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ export async function cacheBasics(cacheName: string, buildAssets: string[]) {
|
|||||||
'first-interaction.',
|
'first-interaction.',
|
||||||
// Main app JS & CSS:
|
// Main app JS & CSS:
|
||||||
'main-app.',
|
'main-app.',
|
||||||
|
// Service worker handler:
|
||||||
|
'offliner.',
|
||||||
// Little icons for the demo images on the homescreen:
|
// Little icons for the demo images on the homescreen:
|
||||||
'icon-demo-',
|
'icon-demo-',
|
||||||
// Site logo:
|
// Site logo:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|||||||
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||||
const HtmlPlugin = require('html-webpack-plugin');
|
const HtmlPlugin = require('html-webpack-plugin');
|
||||||
const ScriptExtHtmlPlugin = require('script-ext-html-webpack-plugin');
|
const ScriptExtHtmlPlugin = require('script-ext-html-webpack-plugin');
|
||||||
const ReplacePlugin = require('webpack-plugin-replace');
|
|
||||||
const CopyPlugin = require('copy-webpack-plugin');
|
const CopyPlugin = require('copy-webpack-plugin');
|
||||||
const WatchTimestampsPlugin = require('./config/watch-timestamps-plugin');
|
const WatchTimestampsPlugin = require('./config/watch-timestamps-plugin');
|
||||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
@@ -20,6 +19,8 @@ function readJson (filename) {
|
|||||||
return JSON.parse(fs.readFileSync(filename));
|
return JSON.parse(fs.readFileSync(filename));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VERSION = readJson('./package.json').version;
|
||||||
|
|
||||||
module.exports = function (_, env) {
|
module.exports = function (_, env) {
|
||||||
const isProd = env.mode === 'production';
|
const isProd = env.mode === 'production';
|
||||||
const nodeModules = path.join(__dirname, 'node_modules');
|
const nodeModules = path.join(__dirname, 'node_modules');
|
||||||
@@ -142,12 +143,6 @@ module.exports = function (_, env) {
|
|||||||
exclude: nodeModules,
|
exclude: nodeModules,
|
||||||
loader: 'ts-loader'
|
loader: 'ts-loader'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
test: /\.jsx?$/,
|
|
||||||
loader: 'babel-loader',
|
|
||||||
// Don't respect any Babel RC files found on the filesystem:
|
|
||||||
options: Object.assign(readJson('.babelrc'), { babelrc: false })
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
// All the codec files define a global with the same name as their file name. `exports-loader` attaches those to `module.exports`.
|
// All the codec files define a global with the same name as their file name. `exports-loader` attaches those to `module.exports`.
|
||||||
test: /\/codecs\/.*\.js$/,
|
test: /\/codecs\/.*\.js$/,
|
||||||
@@ -243,7 +238,9 @@ module.exports = function (_, env) {
|
|||||||
compile: true
|
compile: true
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new AutoSWPlugin({}),
|
new AutoSWPlugin({
|
||||||
|
version: VERSION
|
||||||
|
}),
|
||||||
|
|
||||||
new ScriptExtHtmlPlugin({
|
new ScriptExtHtmlPlugin({
|
||||||
inline: ['first']
|
inline: ['first']
|
||||||
@@ -251,22 +248,12 @@ module.exports = function (_, env) {
|
|||||||
|
|
||||||
// Inline constants during build, so they can be folded by UglifyJS.
|
// Inline constants during build, so they can be folded by UglifyJS.
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
|
VERSION: JSON.stringify(VERSION),
|
||||||
// We set node.process=false later in this config.
|
// We set node.process=false later in this config.
|
||||||
// Here we make sure if (process && process.foo) still works:
|
// Here we make sure if (process && process.foo) still works:
|
||||||
process: '{}'
|
process: '{}'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Babel embeds helpful error messages into transpiled classes that we don't need in production.
|
|
||||||
// Here we replace the constructor and message with a static throw, leaving the message to be DCE'd.
|
|
||||||
// This is useful since it shows the message in SourceMapped code when debugging.
|
|
||||||
isProd && new ReplacePlugin({
|
|
||||||
include: /babel-helper$/,
|
|
||||||
patterns: [{
|
|
||||||
regex: /throw\s+(?:new\s+)?((?:Type|Reference)?Error)\s*\(/g,
|
|
||||||
value: (s, type) => `throw 'babel error'; (`
|
|
||||||
}]
|
|
||||||
}),
|
|
||||||
|
|
||||||
// Copying files via Webpack allows them to be served dynamically by `webpack serve`
|
// Copying files via Webpack allows them to be served dynamically by `webpack serve`
|
||||||
new CopyPlugin([
|
new CopyPlugin([
|
||||||
{ from: 'src/manifest.json', to: 'manifest.json' },
|
{ from: 'src/manifest.json', to: 'manifest.json' },
|
||||||
|
|||||||
Reference in New Issue
Block a user