/** * Fetches the app's font files into assets/fonts/. * * ── WHY THIS SCRIPT IS THE WAY IT IS ──────────────────────────────────────── * The previous version pointed at `googlefonts/nunito/raw/main/fonts/ttf/`, * which no longer exists — that repo now ships variable fonts only. It also * piped whatever came back straight into the .ttf without looking at the HTTP * status. So running it after the URLs died wrote GitHub's 404 page into all * four font files, and every one of them was a 300KB HTML document with a * .ttf extension. * * Nothing complained. React Native does not validate a font asset: Android * silently falls back to Roboto, so the app rendered in the system font while * appearing to have a typeface of its own. That is the failure this script now * refuses to repeat: * * - a non-2xx response aborts instead of being written to disk; * - every download is checked for the TrueType magic number (0x00010000) * before it is kept; * - a file that fails either check is deleted, and the script exits non-zero. * * ── WHY THE FILENAMES LOOK ODD ────────────────────────────────────────────── * They are the fonts' own PostScript names, not IBM's file names. iOS resolves * `fontFamily` against the PostScript name while Android resolves it against * the asset filename, so the two only agree if the file is named after the * PostScript name. IBM abbreviates: SemiBold is `SmBld`, Medium is `Medm`, and * Regular has no suffix at all. Renaming them here is what lets * `fontFamily: 'IBMPlexSans-SmBld'` work on both platforms. * * After running this, re-link and rebuild — the files have to be inside the * binary, so a Metro reload alone will not pick them up: * * node download-fonts.js * npx react-native-asset * npx react-native run-android */ /* eslint-disable no-undef -- A Node build script, not app code: the project's ESLint environment is React Native and does not declare Node globals. */ const fs = require('fs'); const https = require('https'); const path = require('path'); const PLEX = 'https://raw.githubusercontent.com/IBM/plex/master/packages'; /** `as` is the PostScript name — see the note above on why that is the filename. */ const fonts = [ { as: 'IBMPlexSans.ttf', url: `${PLEX}/plex-sans/fonts/complete/ttf/IBMPlexSans-Regular.ttf` }, { as: 'IBMPlexSans-Medm.ttf', url: `${PLEX}/plex-sans/fonts/complete/ttf/IBMPlexSans-Medium.ttf` }, { as: 'IBMPlexSans-SmBld.ttf', url: `${PLEX}/plex-sans/fonts/complete/ttf/IBMPlexSans-SemiBold.ttf` }, { as: 'IBMPlexSans-Bold.ttf', url: `${PLEX}/plex-sans/fonts/complete/ttf/IBMPlexSans-Bold.ttf` }, { as: 'IBMPlexMono.ttf', url: `${PLEX}/plex-mono/fonts/complete/ttf/IBMPlexMono-Regular.ttf` }, { as: 'IBMPlexMono-SmBld.ttf', url: `${PLEX}/plex-mono/fonts/complete/ttf/IBMPlexMono-SemiBold.ttf` }, ]; const dir = path.join(__dirname, 'assets', 'fonts'); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } /** Buffers the body so nothing reaches disk until the status has been checked. */ const fetch = (url, redirects = 0) => new Promise((resolve, reject) => { if (redirects > 5) { reject(new Error('too many redirects')); return; } https .get(url, (response) => { const { statusCode, headers } = response; if (statusCode === 301 || statusCode === 302 || statusCode === 307 || statusCode === 308) { response.resume(); fetch(headers.location, redirects + 1).then(resolve, reject); return; } if (statusCode !== 200) { response.resume(); reject(new Error(`HTTP ${statusCode}`)); return; } const chunks = []; response.on('data', (chunk) => chunks.push(chunk)); response.on('end', () => resolve(Buffer.concat(chunks))); response.on('error', reject); }) .on('error', reject); }); /** * A TrueType file opens with sfntVersion 0x00010000. `OTTO` would be an * OpenType/CFF font, which React Native on Android will not load, and anything * else — an HTML error page, most likely — is not a font at all. */ const isTrueType = (buffer) => buffer.length > 4 && buffer.readUInt32BE(0) === 0x00010000; const run = async () => { let failed = 0; for (const font of fonts) { const dest = path.join(dir, font.as); try { const body = await fetch(font.url); if (!isTrueType(body)) { const magic = body.length > 4 ? body.readUInt32BE(0).toString(16) : 'empty'; throw new Error(`not a TrueType file (magic 0x${magic}, ${body.length} bytes)`); } fs.writeFileSync(dest, body); console.log(` ok ${font.as} (${body.length} bytes)`); } catch (error) { failed += 1; // Leave nothing half-written behind: a stale or truncated file here is // exactly what caused the silent fallback this script exists to prevent. fs.rmSync(dest, { force: true }); console.error(` FAILED ${font.as} — ${error.message}`); } } if (failed > 0) { console.error(`\n${failed} font(s) could not be fetched. Do not build with fonts missing.`); process.exitCode = 1; return; } console.log('\nAll fonts downloaded and verified.'); console.log('Next: npx react-native-asset && npx react-native run-android'); }; void run();