From 4cc93bac3e209c33913b2cf2fc711d45c1bd86df Mon Sep 17 00:00:00 2001 From: Prateek Bhatnagar Date: Thu, 5 Nov 2020 19:32:50 -0800 Subject: [PATCH] Adding blank page for fallback (#1441) * Adding blank page for fallback * switching fallback to 200.html * adding tests * bug fix * fixing tests * Apply suggestions from code review Co-authored-by: Leah Co-authored-by: Jason Miller Co-authored-by: Leah --- .../cli/lib/lib/webpack/render-html-plugin.js | 37 +++++++++--- .../lib/lib/webpack/webpack-client-config.js | 9 +-- packages/cli/package.json | 2 +- packages/cli/sw/index.js | 2 +- packages/cli/tests/images/build.js | 1 + packages/cli/tests/service-worker.test.js | 56 ++++++++++++++++--- yarn.lock | 21 ++++--- 7 files changed, 94 insertions(+), 34 deletions(-) diff --git a/packages/cli/lib/lib/webpack/render-html-plugin.js b/packages/cli/lib/lib/webpack/render-html-plugin.js index 1c2d8b495..55d365e87 100644 --- a/packages/cli/lib/lib/webpack/render-html-plugin.js +++ b/packages/cli/lib/lib/webpack/render-html-plugin.js @@ -9,6 +9,8 @@ const { warn } = require('../../util'); const { info } = require('../../util'); const { PRERENDER_DATA_FILE_NAME } = require('../constants'); +const PREACT_FALLBACK_URL = '/200.html'; + let defaultTemplate = resolve(__dirname, '../../resources/template.html'); function read(path) { @@ -53,10 +55,14 @@ module.exports = async function (config) { writeFileSync(template, content); } - const htmlWebpackConfig = values => { + const htmlWebpackConfig = (values) => { const { url, title, ...routeData } = values; + // Do not create a folder if the url is for a specific file. + const filename = url.endsWith('.html') + ? resolve(dest, url.substring(1)) + : resolve(dest, url.substring(1), 'index.html'); return Object.assign(values, { - filename: resolve(dest, url.substring(1), 'index.html'), + filename, template: `!!ejs-loader?esModule=false!${template}`, minify: isProd && { collapseWhitespace: true, @@ -90,7 +96,9 @@ module.exports = async function (config) { config, url, ssr() { - return config.prerender ? prerender({ cwd, dest, src }, values) : ''; + return config.prerender && url !== PREACT_FALLBACK_URL + ? prerender({ cwd, dest, src }, values) + : ''; }, scriptLoading: 'defer', CLI_DATA: { preRenderData: { url, ...routeData } }, @@ -137,12 +145,21 @@ module.exports = async function (config) { ); } } + /** + * We cache a non SSRed page in service worker so that there is + * no flash of content when user lands on routes other than `/`. + * And we dont have to cache every single html file. + * Go easy on network usage of clients. + */ + !pages.find((page) => page.url === PREACT_FALLBACK_URL) && + pages.push({ url: PREACT_FALLBACK_URL }); - return pages + const resultPages = pages .map(htmlWebpackConfig) - .map(conf => new HtmlWebpackPlugin(conf)) + .map((conf) => new HtmlWebpackPlugin(conf)) .concat([new HtmlWebpackExcludeAssetsPlugin()]) - .concat([...pages.map(page => new PrerenderDataExtractPlugin(page))]); + .concat([...pages.map((page) => new PrerenderDataExtractPlugin(page))]); + return resultPages; }; // Adds a preact_prerender_data in every folder so that the data could be fetched separately. @@ -154,7 +171,11 @@ class PrerenderDataExtractPlugin { this.data_ = JSON.stringify(cliData.preRenderData || {}); } apply(compiler) { - compiler.hooks.emit.tap('PrerenderDataExtractPlugin', compilation => { + compiler.hooks.emit.tap('PrerenderDataExtractPlugin', (compilation) => { + if (this.location_ === `${PREACT_FALLBACK_URL}/`) { + // We dont build prerender data for `200.html`. It can re-use the one for homepage. + return; + } let path = this.location_ + PRERENDER_DATA_FILE_NAME; if (path.startsWith('/')) { path = path.substr(1); @@ -166,3 +187,5 @@ class PrerenderDataExtractPlugin { }); } } + +exports.PREACT_FALLBACK_URL = PREACT_FALLBACK_URL; diff --git a/packages/cli/lib/lib/webpack/webpack-client-config.js b/packages/cli/lib/lib/webpack/webpack-client-config.js index 8b43029a5..2add797cf 100644 --- a/packages/cli/lib/lib/webpack/webpack-client-config.js +++ b/packages/cli/lib/lib/webpack/webpack-client-config.js @@ -228,7 +228,7 @@ function isProd(config) { swSrc: swPath, swDest: 'sw-esm.js', include: [ - /^\/?index\.html$/, + /200\.html$/, /\.esm.js$/, /\.css$/, /\.(png|jpg|svg|gif|webp)$/, @@ -246,12 +246,7 @@ function isProd(config) { prodConfig.plugins.push( new InjectManifest({ swSrc: swPath, - include: [ - /index\.html$/, - /\.js$/, - /\.css$/, - /\.(png|jpg|svg|gif|webp)$/, - ], + include: [/200\.html$/, /\.js$/, /\.css$/, /\.(png|jpg|svg|gif|webp)$/], exclude: [/\.esm\.js$/], }) ); diff --git a/packages/cli/package.json b/packages/cli/package.json index d538b56d4..95da49dcf 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -58,7 +58,7 @@ "preact": "^10.0.0", "preact-render-to-string": "^5.0.6", "preact-router": "^3.0.1", - "puppeteer": "^5.0.0", + "puppeteer": "^5.3.1", "sass-loader": "^10.0.4", "shelljs": "^0.8.3", "sirv": "^1.0.0-next.2" diff --git a/packages/cli/sw/index.js b/packages/cli/sw/index.js index c461eb265..0e2fcac15 100644 --- a/packages/cli/sw/index.js +++ b/packages/cli/sw/index.js @@ -20,7 +20,7 @@ export function setupRouting() { setCatchHandler(({ event }) => { if (isNav(event)) { - return caches.match(getCacheKeyForURL('/index.html')); + return caches.match(getCacheKeyForURL('/200.html')); } return Response.error(); }); diff --git a/packages/cli/tests/images/build.js b/packages/cli/tests/images/build.js index 82ad85fab..0d6f311f7 100644 --- a/packages/cli/tests/images/build.js +++ b/packages/cli/tests/images/build.js @@ -16,6 +16,7 @@ exports.default = exports.full = Object.assign({}, common, { 'bundle.7e56a.css': 901, 'favicon.ico': 15086, 'index.html': 2034, + '200.html': 613, 'manifest.json': 455, 'preact_prerender_data.json': 11, 'push-manifest.json': 812, diff --git a/packages/cli/tests/service-worker.test.js b/packages/cli/tests/service-worker.test.js index 95ba1c9e8..4bd11391c 100644 --- a/packages/cli/tests/service-worker.test.js +++ b/packages/cli/tests/service-worker.test.js @@ -5,12 +5,26 @@ const { sleep } = require('./lib/utils'); const { getServer } = require('./server'); const startChrome = require('./lib/chrome'); +async function enableOfflineMode(page, browser) { + await sleep(2000); // wait for service worker installation. + await page.setOfflineMode(true); + const targets = await browser.targets(); + const serviceWorker = targets.find((t) => t.type() === 'service_worker'); + const serviceWorkerConnection = await serviceWorker.createCDPSession(); + await serviceWorkerConnection.send('Network.enable'); + await serviceWorkerConnection.send('Network.emulateNetworkConditions', { + offline: true, + latency: 0, + downloadThroughput: 0, + uploadThroughput: 0, + }); +} + describe('preact service worker tests', () => { let server, browser, dir; beforeAll(async () => { dir = await create('default'); - browser = await startChrome(); await build(dir, { sw: true, esm: true, @@ -19,9 +33,16 @@ describe('preact service worker tests', () => { server = getServer(dir); }); + beforeEach(async () => { + browser = await startChrome(); + }); + + afterEach(async () => { + await browser.close(); + }); + afterAll(async () => { await server.server.stop(); - await browser.close(); }); it('works offline', async () => { @@ -31,15 +52,14 @@ describe('preact service worker tests', () => { waitUntil: 'networkidle0', }); const initialContent = await page.content(); - await sleep(2000); // wait for service worker installation. - await page.setOfflineMode(true); - await page.reload(); + await enableOfflineMode(page, browser); + await page.reload({ waitUntil: 'networkidle0' }); const offlineContent = await page.content(); await page.waitForSelector('h1'); expect( - await page.$$eval('h1', nodes => nodes.map(n => n.innerText)) + await page.$$eval('h1', (nodes) => nodes.map((n) => n.innerText)) ).toEqual(['Preact App', 'Home']); - expect(offlineContent).toEqual(initialContent); + expect(offlineContent).not.toEqual(initialContent); }); it('should fetch navigation requests with networkFirst', async () => { @@ -64,4 +84,26 @@ describe('preact service worker tests', () => { expect(initialContent).not.toEqual(refreshedContent); expect(refreshedContent.includes(NEW_TITLE)).toEqual(true); }); + + it('should respond with 200.html when offline', async () => { + const swText = await fetch('http://localhost:3000/sw-esm.js').then((res) => + res.text() + ); + // eslint-disable-next-line no-useless-escape + expect(swText).toContain( + 'caches.match((t="/200.html",ce().getCacheKeyForURL(t)))' + ); + const page = await browser.newPage(); + await page.setCacheEnabled(false); + await page.goto('http://localhost:3000', { + waitUntil: 'networkidle0', + }); + await enableOfflineMode(page, browser); + await page.reload({ waitUntil: 'networkidle0' }); + expect( + await page.$$eval('script[type=__PREACT_CLI_DATA__]', (nodes) => + nodes.map((n) => n.innerText) + ) + ).toEqual(['%7B%22preRenderData%22:%7B%22url%22:%22/200.html%22%7D%7D']); + }); }); diff --git a/yarn.lock b/yarn.lock index f9dedff5c..f10cda2b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5343,10 +5343,10 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== -devtools-protocol@0.0.781568: - version "0.0.781568" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.781568.tgz#4cdca90a952d2c77831096ff6cd32695d8715a04" - integrity sha512-9Uqnzy6m6zEStluH9iyJ3iHyaQziFnMnLeC8vK0eN6smiJmIx7+yB64d67C2lH/LZra+5cGscJAJsNXO+MdPMg== +devtools-protocol@0.0.799653: + version "0.0.799653" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.799653.tgz#86fc95ce5bf4fdf4b77a58047ba9d2301078f119" + integrity sha512-t1CcaZbvm8pOlikqrsIM9GOa7Ipp07+4h/q9u0JXBWjPCjHdBl9KkddX87Vv9vBHoBGtwV79sYQNGnQM6iS5gg== dezalgo@^1.0.0, dezalgo@~1.0.3: version "1.0.3" @@ -9618,7 +9618,7 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.0.3, mime@^2.3.1, mime@^2.4.4: +mime@^2.3.1, mime@^2.4.4: version "2.4.6" resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== @@ -11801,16 +11801,15 @@ pupa@^2.0.1: dependencies: escape-goat "^2.0.0" -puppeteer@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-5.2.1.tgz#7f0564f0a5384f352a38c8cc42af875cd87f4ea6" - integrity sha512-PZoZG7u+T6N1GFWBQmGVG162Ak5MAy8nYSVpeeQrwJK2oYUlDWpHEJPcd/zopyuEMTv7DiztS1blgny1txR2qw== +puppeteer@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-5.3.1.tgz#324e190d89f25ac33dba539f57b82a18553f8646" + integrity sha512-YTM1RaBeYrj6n7IlRXRYLqJHF+GM7tasbvrNFx6w1S16G76NrPq7oYFKLDO+BQsXNtS8kW2GxWCXjIMPvfDyaQ== dependencies: debug "^4.1.0" - devtools-protocol "0.0.781568" + devtools-protocol "0.0.799653" extract-zip "^2.0.0" https-proxy-agent "^4.0.0" - mime "^2.0.3" pkg-dir "^4.2.0" progress "^2.0.1" proxy-from-env "^1.0.0"