Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding blank page for fallback #1441

Merged
merged 10 commits into from
Nov 6, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions packages/cli/lib/lib/webpack/render-html-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ 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) {
return readFileSync(resolve(__dirname, path), 'utf-8');
}

module.exports = async function(config) {
module.exports = async function (config) {
const { cwd, dest, isProd, src } = config;
const inProjectTemplatePath = resolve(src, 'template.html');
let template = defaultTemplate;
Expand Down Expand Up @@ -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!${template}`,
minify: isProd && {
collapseWhitespace: true,
Expand Down Expand Up @@ -90,10 +96,12 @@ 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 } }
CLI_DATA: { preRenderData: { url, ...routeData } },
});
};

Expand Down Expand Up @@ -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.
Expand All @@ -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}/`) {
Copy link
Member

Choose a reason for hiding this comment

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

Wondering if we still need this with the custom 200 option.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, this makes sure that the fallback page has no pre-render data.
Thus in this case the pre-render data will always rely on the current 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);
Expand All @@ -166,3 +187,5 @@ class PrerenderDataExtractPlugin {
});
}
}

exports.PREACT_FALLBACK_URL = PREACT_FALLBACK_URL;
11 changes: 3 additions & 8 deletions packages/cli/lib/lib/webpack/webpack-client-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ function isProd(config) {
'process.env.ESM': config.esm,
'process.env.PRERENDER': config.prerender,
}),
new SizePlugin()
new SizePlugin(),
],

optimization: {
Expand Down Expand Up @@ -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)$/,
Expand All @@ -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$/],
})
);
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"preact-compat": "^3.18.5",
"preact-render-to-string": "^5.0.6",
"preact-router": "^3.0.1",
"puppeteer": "^5.0.0",
"puppeteer": "^5.3.1",
"sass-loader": "^9.0.2",
"shelljs": "^0.8.3",
"sirv": "^1.0.0-next.2"
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/sw/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down
1 change: 1 addition & 0 deletions packages/cli/tests/images/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
56 changes: 49 additions & 7 deletions packages/cli/tests/service-worker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
developit marked this conversation as resolved.
Show resolved Hide resolved
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']);
});
});
21 changes: 10 additions & 11 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5333,10 +5333,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"
Expand Down Expand Up @@ -9603,7 +9603,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==
Expand Down Expand Up @@ -11805,16 +11805,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"
Expand Down