Update CBS Mobile App codebase
This commit is contained in:
+124
-31
@@ -1,12 +1,57 @@
|
||||
const https = require('https');
|
||||
/**
|
||||
* 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 = [
|
||||
{ name: 'Nunito-Regular.ttf', url: 'https://github.com/googlefonts/nunito/raw/main/fonts/ttf/Nunito-Regular.ttf' },
|
||||
{ name: 'Nunito-SemiBold.ttf', url: 'https://github.com/googlefonts/nunito/raw/main/fonts/ttf/Nunito-SemiBold.ttf' },
|
||||
{ name: 'Nunito-Bold.ttf', url: 'https://github.com/googlefonts/nunito/raw/main/fonts/ttf/Nunito-Bold.ttf' },
|
||||
{ name: 'Nunito-ExtraBold.ttf', url: 'https://github.com/googlefonts/nunito/raw/main/fonts/ttf/Nunito-ExtraBold.ttf' },
|
||||
{ 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');
|
||||
@@ -15,30 +60,78 @@ if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
function download(url, dest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = fs.createWriteStream(dest);
|
||||
https.get(url, (response) => {
|
||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||
download(response.headers.location, dest).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close(resolve);
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
fs.unlink(dest, () => reject(err));
|
||||
});
|
||||
});
|
||||
}
|
||||
/** 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;
|
||||
}
|
||||
|
||||
Promise.all(fonts.map(font => {
|
||||
const dest = path.join(dir, font.name);
|
||||
console.log(`Downloading ${font.name}...`);
|
||||
return download(font.url, dest);
|
||||
})).then(() => {
|
||||
console.log('All fonts downloaded successfully!');
|
||||
}).catch(err => {
|
||||
console.error('Error downloading fonts:', err);
|
||||
});
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user