webpack-critical-pages
Version:
Automatically generate critical css for a well-defined paging structure on multiple entrypoints.
141 lines (132 loc) • 4.03 kB
text/typescript
import penthouse from "penthouse";
import path from "path";
import fs from "fs";
import bent from "bent";
import crypto, { type BinaryLike } from "crypto";
import type { Contents } from "./webpack-bundle-tracker";
const requestBuffer = bent("buffer");
export function readJSON(filename: string) {
const contents = fs.readFileSync(filename, "utf8");
return JSON.parse(contents);
}
export function getCriticalPages(base: string, apps: string[]) {
const pages = [];
const rootEntries = readJSON(path.join(base, "entrypoints.json"));
for (const entryName of Object.keys(rootEntries)) {
pages.push({
entrypoint: entryName,
exampleUrl: rootEntries[entryName].exampleUrl,
});
}
for (const app of apps) {
const appEntries = readJSON(path.join(base, app, "entrypoints.json"));
for (const entryName of Object.keys(appEntries)) {
pages.push({
entrypoint: app + "/" + entryName,
exampleUrl: appEntries[entryName].exampleUrl,
});
}
}
return pages;
}
export async function extractCriticalCSS(
criticalPages: {
entrypoint: string;
exampleUrl: any;
}[],
source: string,
destination: string,
webpackStats: Contents,
language = "en",
devServerUrl = "http://localhost:8000",
penthouseOptions = {}
) {
function getEntryPointCSS(entrypoint: string) {
let cssString = "";
const grps = webpackStats.chunks[entrypoint]!;
for (const file of grps) {
if (/\.css$/.test(file)) {
cssString += fs.readFileSync(
path.join(source, language, webpackStats.assets[file]!.path)
);
}
}
return cssString;
}
function startNewJob() {
const page = criticalPages.pop();
if (!page) {
return Promise.resolve();
} else if (page.exampleUrl == null) {
return startNewJob();
}
console.log(`extracting page ${page.exampleUrl}...`);
return penthouse({
url: `${devServerUrl}/${language}${page.exampleUrl}`,
cssString: getEntryPointCSS(page.entrypoint),
...penthouseOptions,
})
.then((criticalCss: string) => {
let target = path.join(destination, page.entrypoint + ".critical.css");
let dirname = path.dirname(target);
if (!fs.existsSync(dirname)) fs.mkdirSync(dirname, { recursive: true });
fs.writeFileSync(target, criticalCss);
return startNewJob();
})
.catch((error: Error) => {
console.log("Page fetch was unsuccessful! Error:", error);
process.exit(1);
});
}
try {
await Promise.all([
startNewJob(),
startNewJob(),
startNewJob(),
startNewJob(),
]);
console.log("all done!");
} catch (reason) {
console.error("Error occured", reason);
}
}
export function loadEntrypoints(
base: string,
rootEntryPoints: { [key: string]: string },
apps: (string | null)[]
) {
for (let app of apps) {
if (app === null) app = "";
let appEntries = readJSON(path.join(base, app, "entrypoints.json"));
for (let entryName of Object.keys(appEntries)) {
let normalized = (app + "/" + entryName).replace(/^\/|\/$/g, "");
rootEntryPoints[normalized] =
"./" + path.join(app, appEntries[entryName].file);
}
}
return rootEntryPoints;
}
export async function fetchPageEntries(
pagesToPrecache: string[],
devServerUrl = "http://localhost:8000"
) {
let extraManifestEntries = [];
try {
console.log("calculating page hashes from local dev server...");
for (let i = 0; i < pagesToPrecache.length; i++) {
let pagePath = pagesToPrecache[i];
let fullUrl = devServerUrl + pagePath;
let response = await requestBuffer(fullUrl);
let md5 = crypto.createHash("md5");
md5.update(response as BinaryLike);
extraManifestEntries.push({
url: pagePath,
revision: md5.digest("hex"),
});
}
console.log(extraManifestEntries);
} catch (e) {
console.warn("Could not fetch entries, skipping.", e);
}
return extraManifestEntries;
}