Skip to content

Commit 12b5f75

Browse files
authored
feat(gatsby-plugin-offline): Allow precaching custom pages (#16877)
* Allow precaching custom pages rather than just the offline shell * Document this and add tests
1 parent abf8881 commit 12b5f75

24 files changed

+373
-66
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/*.js
22
!index.js
3+
!src/__tests__/fixtures/public
34
yarn.lock

packages/gatsby-plugin-offline/README.md

+16-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,22 @@ plugins: [`gatsby-plugin-offline`]
2121

2222
## Available options
2323

24-
As of `gatsby-plugin-offline` 3.0.0, the following options are available:
24+
In `gatsby-plugin-offline` 3.x, the following options are available:
25+
26+
- `precachePages` lets you specify pages whose resources should be precached by the service worker, using an array of globs. For example:
27+
28+
```javascript:title=gatsby-config.js
29+
plugins: [
30+
{
31+
resolve: `gatsby-plugin-offline`,
32+
options: {
33+
precachePages: [`/about-us/`, `/projects/*`],
34+
},
35+
},
36+
]
37+
```
38+
39+
Note: while essential resources of specified pages will be precached, such as JavaScript and CSS, non-essential resources such as fonts and images will not be included. Instead, these will be cached at runtime when a user visits a given page that includes these resources.
2540

2641
- `appendScript` lets you specify a file to be appended at the end of the generated service worker (`sw.js`). For example:
2742

packages/gatsby-plugin-offline/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"dependencies": {
1010
"@babel/runtime": "^7.5.5",
1111
"cheerio": "^1.0.0-rc.3",
12+
"glob": "^7.1.4",
1213
"idb-keyval": "^3.2.0",
1314
"lodash": "^4.17.15",
1415
"slash": "^3.0.0",
@@ -19,7 +20,8 @@
1920
"@babel/core": "^7.5.5",
2021
"babel-preset-gatsby-package": "^0.2.3",
2122
"cpx": "^1.5.0",
22-
"cross-env": "^5.2.1"
23+
"cross-env": "^5.2.1",
24+
"rewire": "^4.0.1"
2325
},
2426
"homepage": "https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby-plugin-offline#readme",
2527
"keywords": [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`getPrecachePages correctly matches pages 1`] = `
4+
Array [
5+
"<PROJECT_ROOT>/packages/gatsby-plugin-offline/src/__tests__/fixtures/public/dir1/index.html",
6+
"<PROJECT_ROOT>/packages/gatsby-plugin-offline/src/__tests__/fixtures/public/dir2/index.html",
7+
"<PROJECT_ROOT>/packages/gatsby-plugin-offline/src/__tests__/fixtures/public/index.html",
8+
"<PROJECT_ROOT>/packages/gatsby-plugin-offline/src/__tests__/fixtures/public/test.html",
9+
]
10+
`;
11+
12+
exports[`getPrecachePages correctly matches pages 2`] = `
13+
Array [
14+
"<PROJECT_ROOT>/packages/gatsby-plugin-offline/src/__tests__/fixtures/public/dir1/index.html",
15+
"<PROJECT_ROOT>/packages/gatsby-plugin-offline/src/__tests__/fixtures/public/dir1/page1.html",
16+
"<PROJECT_ROOT>/packages/gatsby-plugin-offline/src/__tests__/fixtures/public/dir1/page2.html",
17+
]
18+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<!-- This is an HTML file. How exciting!
2+
Content of this file is irrelevant; it is used for testing `getPrecachePages` -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<!-- This is an HTML file. How exciting!
2+
Content of this file is irrelevant; it is used for testing `getPrecachePages` -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<!-- This is an HTML file. How exciting!
2+
Content of this file is irrelevant; it is used for testing `getPrecachePages` -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// This is a script. How exciting!
2+
// Content of this file is irrelevant; it is used for testing `getPrecachePages`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/* This is a stylesheet. How exciting!
2+
Content of this file is irrelevant; it is used for testing `getPrecachePages` */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<!-- This is an HTML file. How exciting!
2+
Content of this file is irrelevant; it is used for testing `getPrecachePages` -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<!-- This is an HTML file. How exciting!
2+
Content of this file is irrelevant; it is used for testing `getPrecachePages` -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<!-- This is an HTML file. How exciting!
2+
Content of this file is irrelevant; it is used for testing `getPrecachePages` -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// This is a script. How exciting!
2+
// Content of this file is irrelevant; it is used for testing `getPrecachePages`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/* This is a stylesheet. How exciting!
2+
Content of this file is irrelevant; it is used for testing `getPrecachePages` */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/* This is a stylesheet. How exciting!
2+
Content of this file is irrelevant; it is used for testing `getPrecachePages` */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// This is a script. How exciting!
2+
// Content of this file is irrelevant; it is used for testing `getPrecachePages`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/* This is a stylesheet. How exciting!
2+
Content of this file is irrelevant; it is used for testing `getPrecachePages` */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<!-- This is an HTML file. How exciting!
2+
Content of this file is irrelevant; it is used for testing `getPrecachePages` -->

packages/gatsby-plugin-offline/src/__tests__/gatsby-node.js

+58-30
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
1-
const { onPostBuild } = require(`../gatsby-node`)
1+
const rewire = require(`rewire`)
22
const fs = require(`fs`)
3+
const path = require(`path`)
34

4-
jest.mock(`fs`)
5-
jest.mock(`../get-resources-from-html`, () => () => [])
5+
describe(`getPrecachePages`, () => {
6+
const gatsbyNode = rewire(`../gatsby-node`)
7+
const getPrecachePages = gatsbyNode.__get__(`getPrecachePages`)
68

7-
let swText = ``
9+
it(`correctly matches pages`, () => {
10+
const base = `${__dirname}/fixtures/public`
811

9-
jest.mock(`workbox-build`, () => {
10-
return {
11-
generateSW() {
12-
return Promise.resolve({ count: 1, size: 1, warnings: [] })
13-
},
14-
}
12+
const allPages = getPrecachePages([`**/*`], base)
13+
expect(allPages.map(page => path.normalize(page))).toMatchSnapshot()
14+
15+
const dir1Pages = getPrecachePages([`/dir1/*`], base)
16+
expect(dir1Pages.map(page => path.normalize(page))).toMatchSnapshot()
17+
})
1518
})
1619

1720
describe(`onPostBuild`, () => {
21+
let swText = ``
22+
const gatsbyNode = rewire(`../gatsby-node`)
23+
1824
const componentChunkName = `chunkName`
1925
const chunks = [`chunk1`, `chunk2`]
2026

@@ -26,31 +32,53 @@ describe(`onPostBuild`, () => {
2632
}
2733

2834
// Mock out filesystem functions
29-
fs.readFileSync.mockImplementation(file => {
30-
if (file === `${process.cwd()}/public/webpack.stats.json`) {
31-
return JSON.stringify(stats)
32-
} else if (file === `public/sw.js`) {
33-
return swText
34-
} else if (file.match(/\/sw-append\.js/)) {
35-
return ``
36-
} else {
37-
return jest.requireActual(`fs`).readFileSync(file)
38-
}
39-
})
35+
const mockFs = {
36+
...fs,
4037

41-
fs.appendFileSync.mockImplementation((file, text) => {
42-
swText += text
43-
})
38+
readFileSync(file) {
39+
if (file === `${process.cwd()}/public/webpack.stats.json`) {
40+
return JSON.stringify(stats)
41+
} else if (file === `public/sw.js`) {
42+
return swText
43+
} else if (file.match(/\/sw-append\.js/)) {
44+
return ``
45+
} else {
46+
return fs.readFileSync(file)
47+
}
48+
},
4449

45-
fs.createReadStream.mockImplementation(() => {
46-
return { pipe() {} }
47-
})
50+
appendFileSync(file, text) {
51+
swText += text
52+
},
53+
54+
createReadStream() {
55+
return { pipe() {} }
56+
},
57+
58+
createWriteStream() {},
59+
}
60+
61+
const mockWorkboxBuild = {
62+
generateSW() {
63+
return Promise.resolve({ count: 1, size: 1, warnings: [] })
64+
},
65+
}
4866

49-
fs.createWriteStream.mockImplementation(() => {})
67+
gatsbyNode.__set__(`fs`, mockFs)
68+
gatsbyNode.__set__(`getResourcesFromHTML`, () => [])
69+
gatsbyNode.__set__(`workboxBuild`, mockWorkboxBuild)
70+
gatsbyNode.__set__(`console`, { log() {} })
5071

5172
it(`appends to sw.js`, async () => {
52-
await onPostBuild(
53-
{ pathPrefix: `` },
73+
await gatsbyNode.onPostBuild(
74+
{
75+
pathPrefix: ``,
76+
reporter: {
77+
info(message) {
78+
console.log(message)
79+
},
80+
},
81+
},
5482
{ appendScript: `${__dirname}/fixtures/custom-sw-code.js` }
5583
)
5684

Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
const path = require(`path`)
22
const getResourcesFromHTML = require(`../get-resources-from-html`)
33

4-
const htmlPath = path.resolve(`${__dirname}/index.html`)
4+
const htmlPath = path.resolve(`${__dirname}/fixtures/public/index.html`)
55

66
it(`it extracts resources correctly`, () => {
7-
const resources = getResourcesFromHTML(htmlPath)
7+
const resources = getResourcesFromHTML(htmlPath, ``)
88
expect(resources).toMatchSnapshot()
99
})

packages/gatsby-plugin-offline/src/gatsby-node.js

+62-18
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
const fs = require(`fs`)
2-
const workboxBuild = require(`workbox-build`)
1+
// use `let` to workaround https://github.com/jhnns/rewire/issues/144
2+
let fs = require(`fs`)
3+
let workboxBuild = require(`workbox-build`)
34
const path = require(`path`)
45
const slash = require(`slash`)
6+
const glob = require(`glob`)
57
const _ = require(`lodash`)
68

7-
const getResourcesFromHTML = require(`./get-resources-from-html`)
9+
let getResourcesFromHTML = require(`./get-resources-from-html`)
810

911
exports.createPages = ({ actions }) => {
1012
if (process.env.NODE_ENV === `production`) {
@@ -28,15 +30,44 @@ const readStats = () => {
2830
}
2931
}
3032

31-
const getAssetsForChunks = chunks => {
33+
function getAssetsForChunks(chunks) {
3234
const files = _.flatten(
3335
chunks.map(chunk => readStats().assetsByChunkName[chunk])
3436
)
3537
return _.compact(files)
3638
}
3739

38-
exports.onPostBuild = (args, pluginOptions) => {
39-
const { pathPrefix } = args
40+
function getPrecachePages(globs, base) {
41+
const precachePages = []
42+
43+
globs.forEach(page => {
44+
const matches = glob.sync(base + page)
45+
matches.forEach(path => {
46+
const isDirectory = fs.lstatSync(path).isDirectory()
47+
let precachePath
48+
49+
if (isDirectory && fs.existsSync(`${path}/index.html`)) {
50+
precachePath = `${path}/index.html`
51+
} else if (path.endsWith(`.html`)) {
52+
precachePath = path
53+
} else {
54+
return
55+
}
56+
57+
if (precachePages.indexOf(precachePath) === -1) {
58+
precachePages.push(precachePath)
59+
}
60+
})
61+
})
62+
63+
return precachePages
64+
}
65+
66+
exports.onPostBuild = (
67+
args,
68+
{ precachePages: precachePagesGlobs = [], appendScript = null, workboxConfig }
69+
) => {
70+
const { pathPrefix, reporter } = args
4071
const rootDir = `public`
4172

4273
// Get exact asset filenames for app and offline app shell chunks
@@ -47,16 +78,25 @@ exports.onPostBuild = (args, pluginOptions) => {
4778
])
4879
const appFile = files.find(file => file.startsWith(`app-`))
4980

50-
// Remove the custom prefix (if any) so Workbox can find the files.
51-
// This is added back at runtime (see modifyUrlPrefix) in order to serve
52-
// from the correct location.
53-
const omitPrefix = path => path.slice(pathPrefix.length)
81+
function flat(arr) {
82+
return Array.prototype.flat ? arr.flat() : [].concat(...arr)
83+
}
5484

55-
const criticalFilePaths = getResourcesFromHTML(
56-
`${process.cwd()}/${rootDir}/offline-plugin-app-shell-fallback/index.html`
57-
).map(omitPrefix)
85+
const offlineShellPath = `${process.cwd()}/${rootDir}/offline-plugin-app-shell-fallback/index.html`
86+
const precachePages = [
87+
offlineShellPath,
88+
...getPrecachePages(
89+
precachePagesGlobs,
90+
`${process.cwd()}/${rootDir}`
91+
).filter(page => page !== offlineShellPath),
92+
]
93+
94+
const criticalFilePaths = _.uniq(
95+
flat(precachePages.map(page => getResourcesFromHTML(page, pathPrefix)))
96+
)
5897

5998
const globPatterns = files.concat([
99+
// criticalFilePaths doesn't include HTML pages (we only need this one)
60100
`offline-plugin-app-shell-fallback/index.html`,
61101
...criticalFilePaths,
62102
])
@@ -108,7 +148,7 @@ exports.onPostBuild = (args, pluginOptions) => {
108148

109149
const combinedOptions = {
110150
...options,
111-
...pluginOptions.workboxConfig,
151+
...workboxConfig,
112152
}
113153

114154
const idbKeyvalFile = `idb-keyval-iife.min.js`
@@ -129,18 +169,22 @@ exports.onPostBuild = (args, pluginOptions) => {
129169

130170
fs.appendFileSync(`public/sw.js`, `\n` + swAppend)
131171

132-
if (pluginOptions.appendScript) {
172+
if (appendScript !== null) {
133173
let userAppend
134174
try {
135-
userAppend = fs.readFileSync(pluginOptions.appendScript, `utf8`)
175+
userAppend = fs.readFileSync(appendScript, `utf8`)
136176
} catch (e) {
137177
throw new Error(`Couldn't find the specified offline inject script`)
138178
}
139179
fs.appendFileSync(`public/sw.js`, `\n` + userAppend)
140180
}
141181

142-
console.log(
143-
`Generated ${swDest}, which will precache ${count} files, totaling ${size} bytes.`
182+
reporter.info(
183+
`Generated ${swDest}, which will precache ${count} files, totaling ${size} bytes.\n` +
184+
`The following pages will be precached:\n` +
185+
precachePages
186+
.map(path => path.replace(`${process.cwd()}/public`, ``))
187+
.join(`\n`)
144188
)
145189
})
146190
}

packages/gatsby-plugin-offline/src/get-resources-from-html.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const path = require(`path`)
33
const fs = require(`fs`)
44
const _ = require(`lodash`)
55

6-
module.exports = htmlPath => {
6+
module.exports = (htmlPath, pathPrefix) => {
77
// load index.html to pull scripts/links necessary for proper offline reload
88
let html
99
try {
@@ -41,10 +41,15 @@ module.exports = htmlPath => {
4141
// Don't cache XML files, or external resources (beginning with // or http)
4242
const blackListRegex = /(\.xml$|^\/\/|^http)/
4343

44-
if (!blackListRegex.test(url)) {
44+
// check resource URLs from header tags start with the correct prefix
45+
// (these are not page URLs)
46+
if (!blackListRegex.test(url) && url.startsWith(pathPrefix)) {
4547
criticalFilePaths.push(url.replace(/^\//, ``))
4648
}
4749
})
4850

49-
return _.uniq(criticalFilePaths)
51+
// Remove the custom prefix (if any) so Workbox can find the files.
52+
// This is added back at runtime (see modifyUrlPrefix in gatsby-node.js) in
53+
// order to serve from the correct location.
54+
return _.uniq(criticalFilePaths).map(url => url.slice(pathPrefix.length))
5055
}

0 commit comments

Comments
 (0)