Skip to content

Commit

Permalink
fix: preload fonts (#4450)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrians5j authored Dec 17, 2024
1 parent 017b55b commit f2b573c
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 165 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ describe(`"renderUrl" Function Test`, () => {
renderUrlFunction: async () => {
return {
content: BASE_HTML,
meta: {}
meta: {
interceptedRequests: []
}
};
}
});
Expand Down Expand Up @@ -58,7 +60,9 @@ describe(`"renderUrl" Function Test`, () => {
renderUrlFunction: async () => {
return {
content: BASE_HTML,
meta: {}
meta: {
interceptedRequests: []
}
};
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ describe(`"renderUrl" Function Test`, () => {
renderUrlFunction: async () => {
return {
content: BASE_HTML,
meta: {}
meta: {
interceptedRequests: []
}
};
}
});
Expand Down Expand Up @@ -58,7 +60,9 @@ describe(`"renderUrl" Function Test`, () => {
renderUrlFunction: async () => {
return {
content: BASE_HTML,
meta: {}
meta: {
interceptedRequests: []
}
};
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import chromium from "@sparticuz/chromium";
import puppeteer, { Browser, Page } from "puppeteer-core";
import extractPeLoaderDataFromHtml from "./extractPeLoaderDataFromHtml";
import { RenderResult, RenderUrlCallableParams } from "./types";
import { TagPathLink } from "~/types";

const windowSet = (page: Page, name: string, value: string | boolean) => {
page.evaluateOnNewDocument(`
Object.defineProperty(window, '${name}', {
get() {
return '${value}'
}
})`);
};

export interface File {
type: string;
body: any;
name: string;
meta: {
tags?: TagPathLink[];
[key: string]: any;
};
}

export const defaultRenderUrlFunction = async (
url: string,
params: RenderUrlCallableParams
): Promise<RenderResult> => {
let browser!: Browser;

try {
browser = await puppeteer.launch({
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
headless: chromium.headless,
ignoreHTTPSErrors: true
});

const browserPage = await browser.newPage();

// Can be used to add additional logic - e.g. skip a GraphQL query to be made when in pre-rendering process.
windowSet(browserPage, "__PS_RENDER__", true);

const tenant = params.args.tenant;
if (tenant) {
console.log("Setting tenant (__PS_RENDER_TENANT__) to window object....");
windowSet(browserPage, "__PS_RENDER_TENANT__", tenant);
}

const locale = params.args.locale;
if (locale) {
console.log("Setting locale (__PS_RENDER_LOCALE__) to window object....");
windowSet(browserPage, "__PS_RENDER_LOCALE__", locale);
}

const renderResult: RenderResult = {
content: "",
meta: {
interceptedRequests: [],
apolloState: {},
cachedData: {
apolloGraphQl: [],
peLoaders: []
}
}
};

// Don't load these resources during prerender.
const skipResources = ["image"];
await browserPage.setRequestInterception(true);

browserPage.on("request", request => {
const issuedRequest = {
type: request.resourceType(),
url: request.url(),
aborted: false
};

if (skipResources.includes(issuedRequest.type)) {
issuedRequest.aborted = true;
request.abort();
} else {
request.continue();
}

renderResult.meta.interceptedRequests.push(issuedRequest);
});

// TODO: should be a plugin.
browserPage.on("response", async response => {
const request = response.request();
const url = request.url();
if (url.includes("/graphql") && request.method() === "POST") {
const responses = (await response.json()) as Record<string, any>;
const postData = JSON.parse(request.postData() as string);
const operations = Array.isArray(postData) ? postData : [postData];

for (let i = 0; i < operations.length; i++) {
const { query, variables } = operations[i];

// For now, we're doing a basic @ps(cache: true) match to determine if the
// cache was set true. In the future, if we start introducing additional
// parameters here, we should probably make this parsing smarter.
const mustCache = query.match(/@ps\((cache: true)\)/);

if (mustCache) {
const data = Array.isArray(responses) ? responses[i].data : responses.data;
renderResult.meta.cachedData.apolloGraphQl.push({
query,
variables,
data
});
}
}
return;
}
});

// Load URL and wait for all network requests to settle.
await browserPage.goto(url, { waitUntil: "networkidle0" });

renderResult.content = await browserPage.content();

renderResult.meta.apolloState = await browserPage.evaluate(() => {
// @ts-expect-error
return window.getApolloState();
});

renderResult.meta.cachedData.peLoaders = extractPeLoaderDataFromHtml(renderResult.content);

return renderResult;
} finally {
if (browser) {
// We need to close all open pages first, to prevent browser from hanging when closed.
const pages = await browser.pages();
for (const page of pages) {
await page.close();
}

// This is fixing an issue where the `await browser.close()` would hang indefinitely.
// The "inspiration" for this fix came from the following issue:
// https://github.com/Sparticuz/chromium/issues/85
console.log("Killing browser process...");
const childProcess = browser.process();
if (childProcess) {
childProcess.kill(9);
}

console.log("Browser process killed.");
}
}

// There's no catch block here because errors are already being handled
// in the entrypoint function, located in `./index.ts` file.
};
7 changes: 7 additions & 0 deletions packages/api-prerendering-service/src/render/preloadCss.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { RenderResult } from "./types";

export const preloadCss = (render: RenderResult): void => {
const regex = /<link href="\/static\/css\//gm;
const subst = `<link data-link-preload data-link-preload-type="markup" href="/static/css/`;
render.content = render.content.replace(regex, subst);
};
37 changes: 37 additions & 0 deletions packages/api-prerendering-service/src/render/preloadFonts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { RenderResult } from "~/render/types";

function getFontType(url: string) {
if (url.endsWith(".woff2")) {
return "woff2";
}
if (url.endsWith(".woff")) {
return "woff";
}
if (url.endsWith(".ttf")) {
return "truetype";
}
if (url.endsWith(".otf")) {
return "opentype";
}
if (url.endsWith(".eot")) {
return "embedded-opentype";
}
return "font";
}

export const preloadFonts = (render: RenderResult): void => {
const fontsRequests = render.meta.interceptedRequests.filter(
req => req.type === "font" && req.url
);

const preloadLinks: string = Array.from(fontsRequests)
.map(req => {
return `<link rel="preload" href="${req.url}" as="font" type="font/${getFontType(
req.url
)}" crossorigin="anonymous">`;
})
.join("\n");

// Inject the preload tags into the <head> section
render.content = render.content.replace("</head>", `${preloadLinks}</head>`);
};
7 changes: 7 additions & 0 deletions packages/api-prerendering-service/src/render/preloadJs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { RenderResult } from "~/render/types";

export const preloadJs = (render: RenderResult): void => {
const regex = /<script (src="\/static\/js\/)/gm;
const subst = `<script data-link-preload data-link-preload-type="markup" src="/static/js/`;
render.content = render.content.replace(regex, subst);
};
Loading

0 comments on commit f2b573c

Please sign in to comment.