UNPKG

webpack-critical-pages

Version:

Automatically generate critical css for a well-defined paging structure on multiple entrypoints.

141 lines (132 loc) 4.03 kB
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; }