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

Add preloadFonts option to add <link rel="preload"> tags for fonts #502

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 17 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,22 @@ Use `inlineCss: true` to enable this feature.

TODO: as soon as this feature is stable, it should be enabled by default.

### preloadImages

Use `preloadImages: true` to enable this feature.

Will add preload `<link rel="preload">` tags to the document head for images as described in [the preload critical assets guide of web.dev](https://web.dev/preload-critical-assets/).

Note this only works for images that are self-hosted.

### preloadFonts

Use `preloadFonts: true` to enable this feature.

Will add preload `<link rel="preload">` tags to the document head for fonts as described in [the preload critical assets guide of web.dev](https://web.dev/preload-critical-assets/).

Note when using Google Fonts the react-snap `User-Agent` header will cause only `ttf` fonts to be preloaded. This can be avoided by setting `"userAgent": null` in your configuration. Alternatively you could self-host the `@font-face` declaration so that it will specify modern and fallback fonts regardless of the `User-Agent`.

## ⚠️ Caveats

### Async components
Expand Down Expand Up @@ -349,7 +365,7 @@ See [alternatives](doc/alternatives.md).
## Who uses it

| [![cloud.gov.au](doc/who-uses-it/cloud.gov.au.png)](https://github.com/govau/cloud.gov.au/blob/0187dd78d8f1751923631d3ff16e0fbe4a82bcc6/www/ui/package.json#L29) | [![blacklane](doc/who-uses-it/blacklane.png)](http://m.blacklane.com/) | [![reformma](doc/who-uses-it/reformma.png)](http://reformma.com) |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------- |


## Contributing
Expand Down
73 changes: 56 additions & 17 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const defaultOptions = {
//# even more workarounds
removeStyleTags: false,
preloadImages: false,
preloadFonts: false,
// add async true to script tags
asyncScriptTags: false,
//# another feature creep
Expand Down Expand Up @@ -149,6 +150,7 @@ const preloadResources = opt => {
page,
basePath,
preloadImages,
preloadFonts,
cacheAjaxRequests,
preconnectThirdParty,
http2PushManifest,
Expand All @@ -165,19 +167,22 @@ const preloadResources = opt => {
if (/^http:\/\/localhost/i.test(responseUrl)) {
if (uniqueResources.has(responseUrl)) return;
if (preloadImages && /\.(png|jpg|jpeg|webp|gif|svg)$/.test(responseUrl)) {
const linkAttributes = {
rel: "preload",
as: "image",
href: route
};

if (http2PushManifest) {
http2PushManifestItems.push({
link: route,
as: "image"
});
http2PushManifestItems.push(linkAttributes);
} else {
await page.evaluate(route => {
await page.evaluate(linkAttributes => {
const linkTag = document.createElement("link");
linkTag.setAttribute("rel", "preload");
linkTag.setAttribute("as", "image");
linkTag.setAttribute("href", route);
document.body.appendChild(linkTag);
}, route);
Object.entries(linkAttributes).forEach(([name, value]) => {
linkTag.setAttribute(name, value);
});
document.head.appendChild(linkTag);
}, linkAttributes);
}
} else if (cacheAjaxRequests && ct.includes("json")) {
const json = await response.json();
Expand All @@ -189,7 +194,8 @@ const preloadResources = opt => {
.pop();
if (!ignoreForPreload.includes(fileName)) {
http2PushManifestItems.push({
link: route,
href: route,
rel: "preload",
as: "script"
});
}
Expand All @@ -200,7 +206,8 @@ const preloadResources = opt => {
.pop();
if (!ignoreForPreload.includes(fileName)) {
http2PushManifestItems.push({
link: route,
href: route,
rel: "preload",
as: "style"
});
}
Expand All @@ -218,6 +225,28 @@ const preloadResources = opt => {
document.head.appendChild(linkTag);
}, domain);
}

if (preloadFonts && /\.(woff2?|otf|ttf|eot)$/.test(responseUrl)) {
const linkAttributes = {
rel: "preload",
as: "font",
href: route,
type: ct,
crossorigin: "anonymous"
};

if (http2PushManifest) {
http2PushManifestItems.push(linkAttributes);
} else {
await page.evaluate(linkAttributes => {
const linkTag = document.createElement("link");
Object.entries(linkAttributes).forEach(([name, value]) => {
linkTag.setAttribute(name, value);
});
document.head.appendChild(linkTag);
}, linkAttributes);
}
}
});
return { ajaxCache, http2PushManifestItems };
};
Expand Down Expand Up @@ -514,8 +543,9 @@ const fixParcelChunksIssue = ({
}) => {
return page.evaluate(
(basePath, http2PushManifest, inlineCss) => {
const localScripts = Array.from(document.scripts)
.filter(x => x.src && x.src.startsWith(basePath))
const localScripts = Array.from(document.scripts).filter(
x => x.src && x.src.startsWith(basePath)
);

const mainRegexp = /main\.[\w]{8}\.js/;
const mainScript = localScripts.find(x => mainRegexp.test(x.src));
Expand Down Expand Up @@ -704,11 +734,13 @@ const run = async (userOptions, { fs } = { fs: nativeFs }) => {
beforeFetch: async ({ page, route }) => {
const {
preloadImages,
preloadFonts,
cacheAjaxRequests,
preconnectThirdParty
} = options;
if (
preloadImages ||
preloadFonts ||
cacheAjaxRequests ||
preconnectThirdParty ||
http2PushManifest
Expand All @@ -718,6 +750,7 @@ const run = async (userOptions, { fs } = { fs: nativeFs }) => {
page,
basePath,
preloadImages,
preloadFonts,
cacheAjaxRequests,
preconnectThirdParty,
http2PushManifest,
Expand Down Expand Up @@ -840,7 +873,7 @@ const run = async (userOptions, { fs } = { fs: nativeFs }) => {
);
routePath = normalizePath(routePath);
if (routePath !== newPath) {
console.log(newPath)
console.log(newPath);
console.log(`💬 in browser redirect (${newPath})`);
addToQueue(newRoute);
}
Expand All @@ -855,18 +888,24 @@ const run = async (userOptions, { fs } = { fs: nativeFs }) => {
if (http2PushManifest) {
const manifest = Object.keys(http2PushManifestItems).reduce(
(accumulator, key) => {
if (http2PushManifestItems[key].length !== 0)
if (http2PushManifestItems[key].length !== 0) {
accumulator.push({
source: key,
headers: [
{
key: "Link",
value: http2PushManifestItems[key]
.map(x => `<${x.link}>;rel=preload;as=${x.as}`)
.map(
({ href, ...linkAttributes }) =>
`<${href}>;${Object.entries(linkAttributes)
.map(([name, value]) => `${name}=${value}`)
.join(";")}`
)
.join(",")
}
]
});
}
return accumulator;
},
[]
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"scripts": {
"toc": "yarn run markdown-toc -i doc/recipes.md",
"test": "jest",
"precommit": "prettier --write {*,src/*}.{js,json,css}"
"precommit": "prettier --write {*,src/*}.{js,json,css,md}"
},
"bin": {
"react-snap": "./run.js"
Expand Down
12 changes: 6 additions & 6 deletions run.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ const {
const publicUrl = process.env.PUBLIC_URL || homepage;

const reactScriptsVersion = parseInt(
(devDependencies && devDependencies["react-scripts"])
|| (dependencies && dependencies["react-scripts"])
(devDependencies && devDependencies["react-scripts"]) ||
(dependencies && dependencies["react-scripts"])
);
let fixWebpackChunksIssue;
switch (reactScriptsVersion) {
Expand All @@ -26,13 +26,13 @@ switch (reactScriptsVersion) {
}

const parcel = Boolean(
(devDependencies && devDependencies["parcel-bundler"])
|| (dependencies && dependencies["parcel-bundler"])
(devDependencies && devDependencies["parcel-bundler"]) ||
(dependencies && dependencies["parcel-bundler"])
);

if (parcel) {
if (fixWebpackChunksIssue) {
console.log("Detected both Parcel and CRA. Fixing chunk names for CRA!")
console.log("Detected both Parcel and CRA. Fixing chunk names for CRA!");
} else {
fixWebpackChunksIssue = "Parcel";
}
Expand All @@ -45,4 +45,4 @@ run({
}).catch(error => {
console.error(error);
process.exit(1);
});
});
27 changes: 18 additions & 9 deletions src/puppeteer_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,16 @@ const enableLogging = opt => {
const getLinks = async opt => {
const { page } = opt;
const anchors = await page.evaluate(() =>
Array.from(document.querySelectorAll("a,link[rel='alternate']")).map(anchor => {
if (anchor.href.baseVal) {
const a = document.createElement("a");
a.href = anchor.href.baseVal;
return a.href;
Array.from(document.querySelectorAll("a,link[rel='alternate']")).map(
anchor => {
if (anchor.href.baseVal) {
const a = document.createElement("a");
a.href = anchor.href.baseVal;
return a.href;
}
return anchor.href;
}
return anchor.href;
})
)
);

const iframes = await page.evaluate(() =>
Expand Down Expand Up @@ -184,7 +186,12 @@ const crawl = async opt => {
// Port can be null, therefore we need the null check
const isOnAppPort = port && port.toString() === options.port.toString();

if (hostname === "localhost" && isOnAppPort && !uniqueUrls.has(newUrl) && !streamClosed) {
if (
hostname === "localhost" &&
isOnAppPort &&
!uniqueUrls.has(newUrl) &&
!streamClosed
) {
uniqueUrls.add(newUrl);
enqued++;
queue.write(newUrl);
Expand Down Expand Up @@ -235,7 +242,9 @@ const crawl = async opt => {
sourcemapStore
});
beforeFetch && beforeFetch({ page, route });
await page.setUserAgent(options.userAgent);
if (options.userAgent) {
await page.setUserAgent(options.userAgent);
}
const tracker = createTracker(page);
try {
await page.goto(pageUrl, { waitUntil: "networkidle0" });
Expand Down
17 changes: 17 additions & 0 deletions tests/examples/other/with-font.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<html lang="en">

<head>
<meta charset="utf-8">
<link href="https://fonts.googleapis.com/css2?family=Open+Sans&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Open Sans', sans-serif;
}
</style>
</head>

<body>
Hello open sans
</body>

</html>
25 changes: 18 additions & 7 deletions tests/run.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ describe("many pages", () => {
`/${source}/2/index.html`, // with slash in the end
`/${source}/3/index.html`, // ignores hash
`/${source}/4/index.html`, // ignores query
`/${source}/5/index.html`, // link rel="alternate"
`/${source}/5/index.html` // link rel="alternate"
])
);
});
Expand Down Expand Up @@ -396,6 +396,21 @@ describe("preloadImages", () => {
});
});

describe("preloadFonts", () => {
const source = "tests/examples/other";
const include = ["/with-font.html"];
const { fs, filesCreated, content } = mockFs();
beforeAll(() =>
snapRun(fs, { source, include, preloadFonts: true, userAgent: null })
);
test("adds <link rel=preconnect>", () => {
expect(filesCreated()).toEqual(1);
expect(content(0)).toMatch(
/<link href="https:\/\/fonts.gstatic.com\/s\/opensans.*" rel="preload" as="font" crossorigin="anonymous" type="font\/woff2">/
);
});
});

describe("handles JS errors", () => {
const source = "tests/examples/other";
const include = ["/with-script-error.html"];
Expand Down Expand Up @@ -511,22 +526,18 @@ describe("cacheAjaxRequests", () => {
describe("don't crawl localhost links on different port", () => {
const source = "tests/examples/other";
const include = ["/localhost-links-different-port.html"];

const { fs, filesCreated, names } = mockFs();

beforeAll(() => snapRun(fs, { source, include }));
test("only one file is crawled", () => {
expect(filesCreated()).toEqual(1);
expect(names()).toEqual(
expect.arrayContaining([
`/${source}/localhost-links-different-port.html`
])
expect.arrayContaining([`/${source}/localhost-links-different-port.html`])
);
});

});


describe("svgLinks", () => {
const source = "tests/examples/other";
const include = ["/svg.html"];
Expand Down