Major update for v1 and tests
This commit is contained in:
145
scripts/fetch-fonts.mjs
Normal file
145
scripts/fetch-fonts.mjs
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user