Skip to content

Commit b57379d

Browse files
authored
refractor service worker (#4123)
* 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
1 parent 0022914 commit b57379d

File tree

10 files changed

+632
-367
lines changed

10 files changed

+632
-367
lines changed

.github/workflows/testing.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,4 @@ jobs:
120120
yarn
121121
yarn cypress install
122122
yarn cypress verify
123-
yarn fetch:supporters
124-
yarn fetch:starter-kits
125123
yarn cypress:ci

cypress.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"baseUrl": "http://localhost:3000",
2+
"baseUrl": "http://localhost:4200",
33
"video": false
44
}

cypress/integration/offline_spec.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// https://github.com/cypress-io/cypress-example-recipes/blob/master/examples/server-communication__offline/cypress/integration/offline-spec.js
2+
3+
const goOffline = () => {
4+
cy.log('**go offline**')
5+
.then(() => {
6+
return Cypress.automation('remote:debugger:protocol', {
7+
command: 'Network.enable',
8+
});
9+
})
10+
.then(() => {
11+
return Cypress.automation('remote:debugger:protocol', {
12+
command: 'Network.emulateNetworkConditions',
13+
params: {
14+
offline: true,
15+
latency: -1,
16+
downloadThroughput: -1,
17+
uploadThroughput: -1,
18+
},
19+
});
20+
});
21+
};
22+
23+
const goOnline = () => {
24+
// disable offline mode, otherwise we will break our tests :)
25+
cy.log('**go online**')
26+
.then(() => {
27+
// https://chromedevtools.github.io/devtools-protocol/1-3/Network/#method-emulateNetworkConditions
28+
return Cypress.automation('remote:debugger:protocol', {
29+
command: 'Network.emulateNetworkConditions',
30+
params: {
31+
offline: false,
32+
latency: -1,
33+
downloadThroughput: -1,
34+
uploadThroughput: -1,
35+
},
36+
});
37+
})
38+
.then(() => {
39+
return Cypress.automation('remote:debugger:protocol', {
40+
command: 'Network.disable',
41+
});
42+
});
43+
};
44+
45+
describe('offline', () => {
46+
describe('site', { browser: '!firefox' }, () => {
47+
// make sure we get back online, even if a test fails
48+
// otherwise the Cypress can lose the browser connection
49+
beforeEach(goOnline);
50+
afterEach(goOnline);
51+
52+
it('shows /migrate/ page', () => {
53+
const url = '/migrate/';
54+
const text = 'Migrate';
55+
56+
cy.visit(url);
57+
cy.get('h1').contains(text);
58+
59+
goOffline();
60+
61+
cy.visit(url);
62+
cy.get('h1').contains(text);
63+
64+
// click `guides` link
65+
cy.get('a[title="guides"]').click();
66+
cy.get('h1').contains('Guides');
67+
});
68+
69+
it('open print dialog when accessing /printable url', () => {
70+
const url = '/migrate/printable';
71+
cy.visit(url, {
72+
onBeforeLoad: (win) => {
73+
cy.stub(win, 'print');
74+
},
75+
});
76+
cy.window().then((win) => {
77+
expect(win.print).to.be.calledOnce;
78+
});
79+
});
80+
});
81+
});

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"prebuild": "npm run clean",
4040
"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",
4141
"postbuild": "npm run sitemap",
42-
"build-test": "npm run build && http-server dist/",
42+
"build-test": "npm run build && http-server --port 4200 dist/",
4343
"test": "npm run lint",
4444
"lint": "run-s lint:*",
4545
"lint:js": "npm run lint-js",
@@ -56,7 +56,7 @@
5656
"jest": "jest",
5757
"cypress:open": "cypress open",
5858
"cypress:run": "cypress run",
59-
"cypress:ci": "start-server-and-test http-get://localhost:3000 cypress:run"
59+
"cypress:ci": "start-server-and-test build-test http://localhost:4200 cypress:run"
6060
},
6161
"husky": {
6262
"hooks": {
@@ -106,7 +106,7 @@
106106
"eslint-plugin-react": "^7.21.5",
107107
"front-matter": "^4.0.2",
108108
"html-loader": "^1.3.0",
109-
"html-webpack-plugin": "^4.4.1",
109+
"html-webpack-plugin": "^4.5.0",
110110
"http-server": "^0.12.3",
111111
"husky": "^4.3.0",
112112
"hyperlink": "^4.5.3",
@@ -151,7 +151,7 @@
151151
"webpack-cli": "^4.2.0",
152152
"webpack-dev-server": "^3.2.1",
153153
"webpack-merge": "^5.1.4",
154-
"workbox-webpack-plugin": "^5.1.4"
154+
"workbox-webpack-plugin": "^6.0.0"
155155
},
156156
"dependencies": {
157157
"docsearch.js": "^2.5.2",

src/PrecacheSsgManifestPlugin.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// we need to precache some assets from ssg too
2+
// they're previously handled by require('./src/utilities/find-files-in-dist')(['.css', '.ico', '.svg'])
3+
4+
const { Compilation, sources } = require('webpack');
5+
const getManifestEntriesFromCompilation = require('workbox-webpack-plugin/build/lib/get-manifest-entries-from-compilation');
6+
7+
module.exports = class PrecacheSsgManifestPlugin {
8+
apply(compiler) {
9+
compiler.hooks.thisCompilation.tap(
10+
'PrecacheSsgManifestPlugin',
11+
(compilation) => {
12+
compilation.hooks.processAssets.tapPromise(
13+
{
14+
name: 'PrecacheSsgManifestPlugin',
15+
stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER - 10,
16+
},
17+
async () => {
18+
const { sortedEntries } = await getManifestEntriesFromCompilation(
19+
compilation,
20+
{
21+
// we don't want to include all html pages
22+
// as that would take too many storages
23+
// svg excluded as it's already included with InjectManifest
24+
include: [/\.(ico|css)/i, /app-shell/i],
25+
}
26+
);
27+
compilation.emitAsset(
28+
'ssg-manifest.json',
29+
new sources.RawSource(JSON.stringify(sortedEntries))
30+
);
31+
}
32+
);
33+
}
34+
);
35+
}
36+
};

src/sw.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { precacheAndRoute, matchPrecache } from 'workbox-precaching';
2+
import { registerRoute } from 'workbox-routing';
3+
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
4+
import { CacheFirst, NetworkOnly } from 'workbox-strategies';
5+
import { ExpirationPlugin } from 'workbox-expiration';
6+
import { setDefaultHandler, setCatchHandler } from 'workbox-routing';
7+
import ssgManifest from '../dist/ssg-manifest.json';
8+
9+
// Precache assets built with webpack
10+
precacheAndRoute(self.__WB_MANIFEST);
11+
12+
precacheAndRoute(ssgManifest);
13+
14+
// Precache manifest.json as ssgManifest couldn't catch it
15+
precacheAndRoute([
16+
{
17+
url: '/manifest.json',
18+
revision: '1', // manually update needed when content changed
19+
},
20+
]);
21+
22+
// Cache Google Fonts
23+
registerRoute(
24+
/https:\/\/fonts\.gstatic\.com/,
25+
new CacheFirst({
26+
cacheName: 'google-fonts-cache',
27+
plugins: [
28+
// Ensure that only requests that result in a 200 status are cached
29+
new CacheableResponsePlugin({
30+
statuses: [200],
31+
}),
32+
new ExpirationPlugin({
33+
// Cache for one year
34+
maxAgeSeconds: 60 * 60 * 24 * 365,
35+
maxEntries: 30,
36+
}),
37+
],
38+
})
39+
);
40+
41+
setDefaultHandler(new NetworkOnly());
42+
setCatchHandler(({ event }) => {
43+
switch (event.request.destination) {
44+
case 'document':
45+
return matchPrecache('/app-shell/index.html');
46+
default:
47+
return Response.error();
48+
}
49+
});

webpack.common.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,17 +129,23 @@ module.exports = () => ({
129129
test: /\.woff2?$/,
130130
type: 'asset/resource',
131131
generator: {
132-
filename: 'font/[hash][ext][query]'
132+
filename: 'font/[name].[hash][ext][query]'
133133
}
134134
},
135135
{
136136
test: /\.(jpg|jpeg|png|ico)$/i,
137-
type: 'asset/resource'
137+
type: 'asset/resource',
138+
generator: {
139+
filename: '[name].[hash][ext][query]'
140+
}
138141
},
139142
{
140143
test: /\.svg$/i,
141144
type: 'asset/resource',
142-
exclude: [path.resolve(__dirname, 'src/styles/icons')]
145+
exclude: [path.resolve(__dirname, 'src/styles/icons')],
146+
generator: {
147+
filename: '[name].[hash][ext][query]'
148+
}
143149
},
144150
{
145151
test: /\.svg$/i,

webpack.prod.js

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
// Import External Dependencies
22
const { merge } = require('webpack-merge');
33
const OptimizeCSSAssetsPlugin = require('css-minimizer-webpack-plugin');
4-
const { GenerateSW } = require('workbox-webpack-plugin');
4+
const { InjectManifest } = require('workbox-webpack-plugin');
5+
const path = require('path');
56

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

9-
// find [css, ico, svg] versioned (hashed) files emitted by SSG run
10-
const hashedAssetsBySSGRun = require('./src/utilities/find-files-in-dist')(['.css', '.ico', '.svg']);
11-
1210
module.exports = env => merge(common(env), {
1311
mode: 'production',
1412
target: 'web',
@@ -43,40 +41,13 @@ module.exports = env => merge(common(env), {
4341
]
4442
},
4543
plugins: [
46-
new GenerateSW({
47-
skipWaiting: true,
48-
clientsClaim: true,
44+
new InjectManifest({
45+
swSrc: path.join(__dirname, 'src/sw.js'),
4946
swDest: 'sw.js',
50-
exclude: [/icon_.*\.png/, /printable/, '/robots.txt', ...hashedAssetsBySSGRun],
51-
additionalManifestEntries: [
52-
{
53-
url: '/app-shell/index.html',
54-
revision: new Date().getTime().toString() // dirty hack
55-
},
56-
{
57-
url: '/manifest.json',
58-
revision: '1'
59-
},
60-
...hashedAssetsBySSGRun.map(url => ({
61-
url: '/' + url, // prepend the publicPath
62-
revision: null
63-
}))
64-
],
65-
navigateFallback: '/app-shell/index.html',
66-
navigateFallbackDenylist: [/printable/],
67-
runtimeCaching: [
68-
{
69-
urlPattern: /https:\/\/fonts\.gstatic\.com/, // cache google fonts for one year
70-
handler: 'CacheFirst',
71-
options: {
72-
cacheName: 'google-fonts',
73-
expiration: {
74-
maxAgeSeconds: 60 * 60 * 24 * 365,
75-
maxEntries: 30
76-
}
77-
}
78-
}
79-
],
47+
// exclude license
48+
exclude: [
49+
/license\.txt/i
50+
]
8051
})
8152
]
8253
});

webpack.ssg.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const contentTree = require('./src/_content.json');
1111

1212
// Load Common Configuration
1313
const common = require('./webpack.common.js');
14+
const PrecacheSsgManifestPlugin = require('./src/PrecacheSsgManifestPlugin');
1415

1516
// content tree to path array
1617
const paths = [
@@ -34,7 +35,7 @@ module.exports = env => merge(common(env), {
3435
index: './server.jsx'
3536
},
3637
output: {
37-
filename: '.server/[name].js',
38+
filename: '.server/[name].[contenthash].js',
3839
libraryTarget: 'umd'
3940
},
4041
optimization: {
@@ -131,5 +132,6 @@ module.exports = env => merge(common(env), {
131132
},
132133
],
133134
}),
135+
new PrecacheSsgManifestPlugin()
134136
]
135137
});

0 commit comments

Comments
 (0)