Skip to content

Commit

Permalink
refractor service worker (#4123)
Browse files Browse the repository at this point in the history
* refractor service worker

* exclude license

* update dependencies to webpack5 compatible

* improve sw

* downgrade html-webpack-plugin

* precache manifest.json

* run e2e test against production

* set port

* add e2e tests for offline

* clean wait

* cleanup action

* upgrade workbox-webpack-plugin to v6

* revert async function

* clean for cypress v6

* fix e2e test

* clean test
  • Loading branch information
chenxsan committed Dec 3, 2020
1 parent 0022914 commit b57379d
Show file tree
Hide file tree
Showing 10 changed files with 632 additions and 367 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/testing.yml
Expand Up @@ -120,6 +120,4 @@ jobs:
yarn
yarn cypress install
yarn cypress verify
yarn fetch:supporters
yarn fetch:starter-kits
yarn cypress:ci
2 changes: 1 addition & 1 deletion cypress.json
@@ -1,4 +1,4 @@
{
"baseUrl": "http://localhost:3000",
"baseUrl": "http://localhost:4200",
"video": false
}
81 changes: 81 additions & 0 deletions cypress/integration/offline_spec.js
@@ -0,0 +1,81 @@
// https://github.com/cypress-io/cypress-example-recipes/blob/master/examples/server-communication__offline/cypress/integration/offline-spec.js

const goOffline = () => {
cy.log('**go offline**')
.then(() => {
return Cypress.automation('remote:debugger:protocol', {
command: 'Network.enable',
});
})
.then(() => {
return Cypress.automation('remote:debugger:protocol', {
command: 'Network.emulateNetworkConditions',
params: {
offline: true,
latency: -1,
downloadThroughput: -1,
uploadThroughput: -1,
},
});
});
};

const goOnline = () => {
// disable offline mode, otherwise we will break our tests :)
cy.log('**go online**')
.then(() => {
// https://chromedevtools.github.io/devtools-protocol/1-3/Network/#method-emulateNetworkConditions
return Cypress.automation('remote:debugger:protocol', {
command: 'Network.emulateNetworkConditions',
params: {
offline: false,
latency: -1,
downloadThroughput: -1,
uploadThroughput: -1,
},
});
})
.then(() => {
return Cypress.automation('remote:debugger:protocol', {
command: 'Network.disable',
});
});
};

describe('offline', () => {
describe('site', { browser: '!firefox' }, () => {
// make sure we get back online, even if a test fails
// otherwise the Cypress can lose the browser connection
beforeEach(goOnline);
afterEach(goOnline);

it('shows /migrate/ page', () => {
const url = '/migrate/';
const text = 'Migrate';

cy.visit(url);
cy.get('h1').contains(text);

goOffline();

cy.visit(url);
cy.get('h1').contains(text);

// click `guides` link
cy.get('a[title="guides"]').click();
cy.get('h1').contains('Guides');
});

it('open print dialog when accessing /printable url', () => {
const url = '/migrate/printable';
cy.visit(url, {
onBeforeLoad: (win) => {
cy.stub(win, 'print');
},
});
cy.window().then((win) => {
expect(win.print).to.be.calledOnce;
});
});
});
});
8 changes: 4 additions & 4 deletions package.json
Expand Up @@ -39,7 +39,7 @@
"prebuild": "npm run clean",
"build": "run-s fetch-repos fetch printable content && cross-env NODE_ENV=production webpack --config webpack.ssg.js && run-s clean-printable content && cross-env NODE_ENV=production webpack --config webpack.prod.js",
"postbuild": "npm run sitemap",
"build-test": "npm run build && http-server dist/",
"build-test": "npm run build && http-server --port 4200 dist/",
"test": "npm run lint",
"lint": "run-s lint:*",
"lint:js": "npm run lint-js",
Expand All @@ -56,7 +56,7 @@
"jest": "jest",
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"cypress:ci": "start-server-and-test http-get://localhost:3000 cypress:run"
"cypress:ci": "start-server-and-test build-test http://localhost:4200 cypress:run"
},
"husky": {
"hooks": {
Expand Down Expand Up @@ -106,7 +106,7 @@
"eslint-plugin-react": "^7.21.5",
"front-matter": "^4.0.2",
"html-loader": "^1.3.0",
"html-webpack-plugin": "^4.4.1",
"html-webpack-plugin": "^4.5.0",
"http-server": "^0.12.3",
"husky": "^4.3.0",
"hyperlink": "^4.5.3",
Expand Down Expand Up @@ -151,7 +151,7 @@
"webpack-cli": "^4.2.0",
"webpack-dev-server": "^3.2.1",
"webpack-merge": "^5.1.4",
"workbox-webpack-plugin": "^5.1.4"
"workbox-webpack-plugin": "^6.0.0"
},
"dependencies": {
"docsearch.js": "^2.5.2",
Expand Down
36 changes: 36 additions & 0 deletions src/PrecacheSsgManifestPlugin.js
@@ -0,0 +1,36 @@
// we need to precache some assets from ssg too
// they're previously handled by require('./src/utilities/find-files-in-dist')(['.css', '.ico', '.svg'])

const { Compilation, sources } = require('webpack');
const getManifestEntriesFromCompilation = require('workbox-webpack-plugin/build/lib/get-manifest-entries-from-compilation');

module.exports = class PrecacheSsgManifestPlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap(
'PrecacheSsgManifestPlugin',
(compilation) => {
compilation.hooks.processAssets.tapPromise(
{
name: 'PrecacheSsgManifestPlugin',
stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER - 10,
},
async () => {
const { sortedEntries } = await getManifestEntriesFromCompilation(
compilation,
{
// we don't want to include all html pages
// as that would take too many storages
// svg excluded as it's already included with InjectManifest
include: [/\.(ico|css)/i, /app-shell/i],
}
);
compilation.emitAsset(
'ssg-manifest.json',
new sources.RawSource(JSON.stringify(sortedEntries))
);
}
);
}
);
}
};
49 changes: 49 additions & 0 deletions src/sw.js
@@ -0,0 +1,49 @@
import { precacheAndRoute, matchPrecache } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
import { CacheFirst, NetworkOnly } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { setDefaultHandler, setCatchHandler } from 'workbox-routing';
import ssgManifest from '../dist/ssg-manifest.json';

// Precache assets built with webpack
precacheAndRoute(self.__WB_MANIFEST);

precacheAndRoute(ssgManifest);

// Precache manifest.json as ssgManifest couldn't catch it
precacheAndRoute([
{
url: '/manifest.json',
revision: '1', // manually update needed when content changed
},
]);

// Cache Google Fonts
registerRoute(
/https:\/\/fonts\.gstatic\.com/,
new CacheFirst({
cacheName: 'google-fonts-cache',
plugins: [
// Ensure that only requests that result in a 200 status are cached
new CacheableResponsePlugin({
statuses: [200],
}),
new ExpirationPlugin({
// Cache for one year
maxAgeSeconds: 60 * 60 * 24 * 365,
maxEntries: 30,
}),
],
})
);

setDefaultHandler(new NetworkOnly());
setCatchHandler(({ event }) => {
switch (event.request.destination) {
case 'document':
return matchPrecache('/app-shell/index.html');
default:
return Response.error();
}
});
12 changes: 9 additions & 3 deletions webpack.common.js
Expand Up @@ -129,17 +129,23 @@ module.exports = () => ({
test: /\.woff2?$/,
type: 'asset/resource',
generator: {
filename: 'font/[hash][ext][query]'
filename: 'font/[name].[hash][ext][query]'
}
},
{
test: /\.(jpg|jpeg|png|ico)$/i,
type: 'asset/resource'
type: 'asset/resource',
generator: {
filename: '[name].[hash][ext][query]'
}
},
{
test: /\.svg$/i,
type: 'asset/resource',
exclude: [path.resolve(__dirname, 'src/styles/icons')]
exclude: [path.resolve(__dirname, 'src/styles/icons')],
generator: {
filename: '[name].[hash][ext][query]'
}
},
{
test: /\.svg$/i,
Expand Down
45 changes: 8 additions & 37 deletions webpack.prod.js
@@ -1,14 +1,12 @@
// Import External Dependencies
const { merge } = require('webpack-merge');
const OptimizeCSSAssetsPlugin = require('css-minimizer-webpack-plugin');
const { GenerateSW } = require('workbox-webpack-plugin');
const { InjectManifest } = require('workbox-webpack-plugin');
const path = require('path');

// Load Common Configuration
const common = require('./webpack.common.js');

// find [css, ico, svg] versioned (hashed) files emitted by SSG run
const hashedAssetsBySSGRun = require('./src/utilities/find-files-in-dist')(['.css', '.ico', '.svg']);

module.exports = env => merge(common(env), {
mode: 'production',
target: 'web',
Expand Down Expand Up @@ -43,40 +41,13 @@ module.exports = env => merge(common(env), {
]
},
plugins: [
new GenerateSW({
skipWaiting: true,
clientsClaim: true,
new InjectManifest({
swSrc: path.join(__dirname, 'src/sw.js'),
swDest: 'sw.js',
exclude: [/icon_.*\.png/, /printable/, '/robots.txt', ...hashedAssetsBySSGRun],
additionalManifestEntries: [
{
url: '/app-shell/index.html',
revision: new Date().getTime().toString() // dirty hack
},
{
url: '/manifest.json',
revision: '1'
},
...hashedAssetsBySSGRun.map(url => ({
url: '/' + url, // prepend the publicPath
revision: null
}))
],
navigateFallback: '/app-shell/index.html',
navigateFallbackDenylist: [/printable/],
runtimeCaching: [
{
urlPattern: /https:\/\/fonts\.gstatic\.com/, // cache google fonts for one year
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts',
expiration: {
maxAgeSeconds: 60 * 60 * 24 * 365,
maxEntries: 30
}
}
}
],
// exclude license
exclude: [
/license\.txt/i
]
})
]
});
4 changes: 3 additions & 1 deletion webpack.ssg.js
Expand Up @@ -11,6 +11,7 @@ const contentTree = require('./src/_content.json');

// Load Common Configuration
const common = require('./webpack.common.js');
const PrecacheSsgManifestPlugin = require('./src/PrecacheSsgManifestPlugin');

// content tree to path array
const paths = [
Expand All @@ -34,7 +35,7 @@ module.exports = env => merge(common(env), {
index: './server.jsx'
},
output: {
filename: '.server/[name].js',
filename: '.server/[name].[contenthash].js',
libraryTarget: 'umd'
},
optimization: {
Expand Down Expand Up @@ -131,5 +132,6 @@ module.exports = env => merge(common(env), {
},
],
}),
new PrecacheSsgManifestPlugin()
]
});

1 comment on commit b57379d

@vercel
Copy link

@vercel vercel bot commented on b57379d Dec 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.