@photogabble/eleventy-plugin-font-subsetting
Version:
Font Subsetting made easier for Eleventy.js
202 lines (180 loc) • 5.85 kB
JavaScript
import { spawn } from "node:child_process";
import chalk from "chalk";
import path from "node:path";
import fs from "node:fs";
import CharacterSet from "characterset";
/**
* Uses the system which command to check if a given command exists in the users' path.
* This is of course linux/unix specific and will need updating if this is deployed on
* a windows box... or there might be a node built in I am unaware of that does this??
*
* @param command {string}
* @returns {Promise<boolean>}
*/
function checkCommandExists(command) {
return new Promise((resolve) => {
const processCheck = spawn("which", [command]);
processCheck.on("exit", (code) => resolve(code === 0));
});
}
/**
* Checks if pyftsubset command exists and is executable.
*
* @returns {Promise<boolean>}
*/
function checkPyftsubset() {
return new Promise((resolve) => {
spawn("pyftsubset", ["--help"])
.on("error", () => resolve(false))
.on("exit", (code) => resolve(code === 0));
});
}
const rootPath = (p) => path.resolve(process.env.ELEVENTY_ROOT, p);
const memoryCache = new Map();
/**
*
* @param { import('@11ty/eleventy/src/UserConfig') } eleventyConfig
* @param { import('@photogabble/eleventy-plugin-font-subsetting').EleventyPluginFontSubsettingOptions } options
*/
export default function (eleventyConfig, options) {
/** @var { import('@photogabble/eleventy-plugin-font-subsetting').EleventyPluginFontSubsettingOptions } opts */
const opts = Object.assign(
{
dist: null,
enabled: true,
srcFiles: [],
cache: {
get: (key) => memoryCache.get(key),
set: (key, value) => memoryCache.set(key, value),
has: (key) => memoryCache.has(key),
},
cacheKey: "font-subsetting",
quiet: false,
},
options,
);
if (opts.dist) eleventyConfig.addPassthroughCopy(opts.dist);
if (opts.enabled === false) return;
const glyphs = {
chars: new Set(),
add(text) {
for (let char of text) {
if ([" ", "\n", "\r", "\t"].includes(char) === false)
this.chars.add(char);
}
},
getUnique() {
const out = Array.from(this.chars);
out.sort();
return out.join("");
},
};
eleventyConfig.addTransform("identifyGlyphs", (content, outputPath) => {
if (outputPath.endsWith(".html")) glyphs.add(content);
return content;
});
eleventyConfig.on("eleventy.after", async () => {
const log = (...args) => {
if (!opts.quiet) {
console.log(...args);
}
};
const cachedUnicodeHexRange = opts.cache.get("cs");
const cs = new CharacterSet(glyphs.getUnique());
const unicodeHexRange = cs.toHexRangeString();
const rootDist = opts.dist ? rootPath(opts.dist) : false;
let srcFiles = (opts.srcFiles ?? []).map((src) => {
const info = path.parse(src);
const dir = rootDist ? rootDist : info.dir;
return {
src: rootPath(src),
dist: `${dir}/${info.name}.subset${info.ext}`,
};
});
// If we have a cached unicode hex range that's identical to what has been discovered then we do not need to
// rebuild the font files unless a subset font file is missing.
if (cachedUnicodeHexRange && cachedUnicodeHexRange === unicodeHexRange) {
srcFiles = srcFiles.filter((file) => fs.existsSync(file.dist) === false);
if (srcFiles.length === 0) {
log(
chalk.blue("[@photogabble/subsetter]"),
chalk.green("[OK]"),
"Matching cached charset found, not rebuilding fonts",
);
return;
}
}
log(
chalk.blue("[@photogabble/subsetter]"),
`Identified ${glyphs.chars.size} unique glyphs in use, creating subset`,
);
log(
chalk.blue("[@photogabble/subsetter]"),
"Codepoint Range:",
unicodeHexRange,
);
log(
chalk.blue("[@photogabble/subsetter]"),
`Subsetting ${srcFiles.length} font files.`,
);
// If we must run, first check if pyftsubset is available in our path
if ((await checkPyftsubset()) === false) {
console.log(
chalk.blue("[@photogabble/subsetter]"),
chalk.red("[ERROR]"),
'Unable to locate pyftsubset, please install via "pip install fonttools"',
);
return;
}
const promises = [];
for (const file of srcFiles) {
promises.push(
new Promise((resolve, reject) => {
log(
chalk.blue("[@photogabble/subsetter]"),
chalk.yellow("[..]"),
`Processing: ${file.src}`,
);
const buildProcess = spawn(
"pyftsubset",
[
file.src,
`--output-file=${file.dist}`,
`--unicodes=${unicodeHexRange}`,
"--flavor=woff2",
],
{ stdio: opts.quiet ? "ignore" : "inherit" },
);
// Reject promise if spawn fails
buildProcess.on("error", reject);
// Resolve/reject promise based on exit code
buildProcess.on("close", (code) => {
if (code === 0) {
log(
chalk.blue("[@photogabble/subsetter]"),
chalk.green("[OK]"),
`Saved: ${file.dist}`,
);
resolve();
} else {
reject(
new Error(
`pyftsubset failed for ${file.src} with exit code ${code}`,
),
);
}
});
}),
);
}
// Let Promise.all handle potential rejections
await Promise.all(promises);
// Cache unicode hex range for 10 years (forever...)
opts.cache.set("cs", unicodeHexRange, 86400 * 365 * 10);
log(
chalk.blue("[@photogabble/subsetter]"),
chalk.green("[OK]"),
`Complete`,
);
});
}