146 lines
4.6 KiB
JavaScript
146 lines
4.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Downloads TTF font files for the Google Fonts used in the editor and saves
|
|
* them under ./fonts/. server.js scans that directory at startup and calls
|
|
* registerFont() for each file.
|
|
*
|
|
* Why we do this:
|
|
* node-canvas does not pick up Google Fonts automatically. It only knows
|
|
* about fonts that are either (a) registered explicitly via registerFont()
|
|
* or (b) installed system-wide and discoverable via fontconfig. Without
|
|
* this script, the server falls back to whatever Cairo can find for
|
|
* "Roboto", "Bebas Neue", etc., which is usually nothing — exports get
|
|
* substituted to a generic sans-serif and look nothing like the preview.
|
|
*
|
|
* Source:
|
|
* We pull TTFs from the Fontsource jsDelivr CDN. URLs look like
|
|
* https://cdn.jsdelivr.net/fontsource/fonts/<slug>@latest/latin-400-normal.ttf
|
|
* Slugs are the lowercase, hyphenated family name. The script asks for two
|
|
* weights per family (regular + bold) and falls through gracefully if a
|
|
* weight isn't published.
|
|
*
|
|
* Usage:
|
|
* npm run fetch-fonts
|
|
*
|
|
* Re-running is cheap: existing files are skipped unless --force is passed.
|
|
*/
|
|
|
|
import { mkdir, writeFile, access } from 'node:fs/promises';
|
|
import { constants as fsConstants } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = join(__dirname, '..');
|
|
const FONTS_DIR = join(ROOT, 'fonts');
|
|
|
|
const FORCE = process.argv.includes('--force');
|
|
|
|
// Families used by the editor (FONTS in src/constants/fonts.js + the two
|
|
// CSS-level defaults). Keep this list in sync with that file.
|
|
const FAMILIES = [
|
|
'Roboto',
|
|
'Open Sans',
|
|
'Lato',
|
|
'Montserrat',
|
|
'Oswald',
|
|
'Raleway',
|
|
'Poppins',
|
|
'Roboto Condensed',
|
|
'Source Sans 3',
|
|
'Roboto Slab',
|
|
'Merriweather',
|
|
'Ubuntu',
|
|
'Playfair Display',
|
|
'Nunito',
|
|
'Rubik',
|
|
'Work Sans',
|
|
'Lora',
|
|
'Fira Sans',
|
|
'Barlow',
|
|
'Bebas Neue',
|
|
'DM Sans',
|
|
'Space Mono',
|
|
// Display + handwritten faces used by the Pawfectly Yours theme. Pacifico is
|
|
// the brand logo face; Caveat is a friendly script fallback exposed in the
|
|
// text-tab font picker so users can match the brand voice in their designs.
|
|
'Pacifico',
|
|
'Caveat',
|
|
];
|
|
|
|
const WEIGHTS = [
|
|
{ weight: 400, suffix: 'Regular' },
|
|
{ weight: 700, suffix: 'Bold' },
|
|
];
|
|
|
|
const familyToSlug = (family) => family.toLowerCase().replace(/\s+/g, '-');
|
|
|
|
// Filename convention used by server.js's registration scanner:
|
|
// "Family_Name-Variant.ttf" → family = "Family Name", variant = "Variant".
|
|
// Underscore-vs-space is so the file system stays portable.
|
|
const familyToFileBase = (family) => family.replace(/\s+/g, '_');
|
|
|
|
async function fileExists(path) {
|
|
try {
|
|
await access(path, fsConstants.R_OK);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function fetchOne(family, weight, suffix) {
|
|
const slug = familyToSlug(family);
|
|
const url = `https://cdn.jsdelivr.net/fontsource/fonts/${slug}@latest/latin-${weight}-normal.ttf`;
|
|
const dest = join(FONTS_DIR, `${familyToFileBase(family)}-${suffix}.ttf`);
|
|
|
|
if (!FORCE && await fileExists(dest)) {
|
|
return { family, weight, status: 'skipped', dest };
|
|
}
|
|
|
|
const res = await fetch(url);
|
|
if (!res.ok) {
|
|
return { family, weight, status: 'missing', dest, code: res.status };
|
|
}
|
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
await writeFile(dest, buf);
|
|
return { family, weight, status: 'fetched', dest, bytes: buf.length };
|
|
}
|
|
|
|
async function main() {
|
|
await mkdir(FONTS_DIR, { recursive: true });
|
|
|
|
let fetched = 0, skipped = 0, missing = 0;
|
|
|
|
for (const family of FAMILIES) {
|
|
for (const { weight, suffix } of WEIGHTS) {
|
|
try {
|
|
const r = await fetchOne(family, weight, suffix);
|
|
if (r.status === 'fetched') {
|
|
fetched++;
|
|
console.log(`✓ ${family} ${weight} → ${r.dest} (${(r.bytes / 1024).toFixed(0)} KB)`);
|
|
} else if (r.status === 'skipped') {
|
|
skipped++;
|
|
console.log(`· ${family} ${weight} (already present)`);
|
|
} else {
|
|
missing++;
|
|
console.log(`! ${family} ${weight} HTTP ${r.code} — variant not published`);
|
|
}
|
|
} catch (err) {
|
|
missing++;
|
|
console.log(`! ${family} ${weight} ${err.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`\nDone. Fetched ${fetched}, skipped ${skipped}, missing ${missing}.`);
|
|
if (missing > 0) {
|
|
console.log('Missing variants are not fatal — the server will fall back to other registered weights.');
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('Font fetch failed:', err);
|
|
process.exit(1);
|
|
});
|