Skip to content

Instantly share code, notes, and snippets.

@bartvanandel
Created October 3, 2022 11:56
Show Gist options
  • Save bartvanandel/19921f48ce134c393bb38bb2c8887247 to your computer and use it in GitHub Desktop.
Save bartvanandel/19921f48ce134c393bb38bb2c8887247 to your computer and use it in GitHub Desktop.
Font download script, useful for using fonts as binary dependencies in your React Native app.

Introduction

We were facing an issue where we need a font to be present at build time for a React Native app. This font is essentially a binary dependency of our app, which we'd rather not add to our git repo (in general, this is not the best practice). There exist packages that can provide certain fonts, however these were either too unorganized for our taste, or didn't work with React-Native / Expo. So, we searched for a solution to just download the fonts at npm install time.

Solution

Our solution is this here "downloadFonts.mjs" module.

Usage:

  • Add "downloadFonts.mjs" to a "scripts" folder inside your package.

  • You'll need to install the following (dev) dependencies with the current version of the script:

    npm install --save-dev extract-zip fdir tempy lodash
  • In "package.json", add the following scripts (merge with your existing scripts):

      "scripts": {
        "download:fonts": "node scripts/downloadFonts.mjs --clean"
        "postinstall": "npm run download:fonts"
      }
  • Add a rule to your ".gitignore" file to ignore "assets/fonts/"

That's it, really!

Now anytime you run npm install, the fonts will be downloaded and extracted to the "assets/fonts/xxx" directory (see script for specifics). Additionally, a javascript file will be generated, exporting a React hook that can be used to load the fonts at startup of your React Native app.

import { constants as fsConstants } from 'node:fs';
import fs from 'node:fs/promises';
import https from 'node:https';
import path from 'node:path';
import url from 'node:url';
import extractZip from 'extract-zip';
import { fdir } from 'fdir';
import { temporaryDirectoryTask } from 'tempy';
import _ from 'lodash';
const downloadFile = async (url, dstPath) => {
console.info('[DownloadFonts] Downloading:', url, '=>', dstPath);
const fh = await fs.open(dstPath, 'wx');
const writeStream = fh.createWriteStream();
return new Promise((resolve, reject) => {
const req = https.request(url, { method: 'get' }, (res) => {
res.pipe(writeStream);
res.on('error', reject);
writeStream.on('finish', resolve);
});
req.end();
});
};
const extractFile = async (archivePath, dstDir) => {
console.info('[DownloadFonts] Extracting', archivePath, '=>', dstDir);
return await extractZip(archivePath, { dir: dstDir });
};
const generateFontIndexFile = (fontFiles) => {
return [
'// !!! DO NOT MODIFY THIS FILE !!!',
'// !!! it is generated by our font download script !!!',
'// !!! at package install time, and ignored by git !!!',
'',
'const fontFiles = {',
...fontFiles.map((p) => {
const ext = path.extname(p);
const filenameWithoutExt = path.basename(p, ext);
return ` '${filenameWithoutExt}': require('./${p}'),`;
}),
'};',
'',
'export default fontFiles;',
'',
]
.map((l) => ` ${l}`)
.join('\n');
};
/**
* Downloads a font from Google Fonts.
*
* The file is downloaded as a .zip file, so we need to uncompress it to get its contents.
*
* @param {string} fontFamily
*/
const downloadGoogleFont = async (fontFamily, dstDir) => {
const url = `https://fonts.google.com/download?family=${encodeURIComponent(fontFamily)}`;
return await temporaryDirectoryTask(async (tmpDir) => {
console.info('[DownloadFonts] Using temporary dir:', tmpDir);
try {
// Download font archive
const fontZipPath = path.join(tmpDir, 'font.zip');
await downloadFile(url, fontZipPath);
// Extract font archive
const extractDir = path.join(tmpDir, 'extracted');
await extractFile(fontZipPath, extractDir);
// Gather paths of extracted files
const crawler = new fdir()
//.withBasePath().withFullPaths()
.glob('**/*.*');
const fontFiles = await crawler.crawl(extractDir).withPromise();
// Remove pre-existing target dir (use with care!)
if (process.argv.includes('--clean')) {
try {
await fs.access(dstDir, fsConstants.F_OK);
try {
console.info('[DownloadFonts] Removing existing target dir:', dstDir);
await fs.rm(dstDir, { recursive: true });
} catch (err) {
console.error('[DownloadFonts] Failed to remove existing target dir:', err);
process.exit(1);
}
} catch (err) {}
}
try {
await fs.access(dstDir, fsConstants.F_OK);
console.info('[DownloadFonts] Reusing existing target dir:', dstDir);
} catch (err) {
console.info('[DownloadFonts] Creating target dir:', dstDir);
await fs.mkdir(dstDir, { recursive: true });
}
// Copy to the target dir
console.info('[DownloadFonts] Copying', fontFiles.length, 'files ...');
const maxFilenameLength = fontFiles.map((p) => p.length).reduce((acc, cur) => Math.max(acc, cur), 0);
await Promise.all(
fontFiles.map(async (filePath) => {
const srcFile = path.join(extractDir, filePath);
const dstFile = path.join(dstDir, filePath);
console.debug(' ', filePath.padEnd(maxFilenameLength, ' '), '=>', dstFile);
await fs.copyFile(srcFile, dstFile);
})
);
// Generate index file
console.info('[DownloadFonts] Generating index file ...');
const indexFile = generateFontIndexFile(fontFiles.filter((p) => /\.(ttf|otf)$/i.test(p)));
await fs.writeFile(path.join(dstDir, 'index.ts'), indexFile);
console.info('[DownloadFonts] Use this to load the fonts:');
console.info('');
console.info(` const [fontsLoaded] = useFonts(require('assets/fonts/${fontFamily}').default);`);
console.info('');
console.info('[DownloadFonts] Done.');
} catch (err) {
console.error('[DownloadFonts] Error:', err);
process.exit(1);
}
});
};
const main = async () => {
const scriptDir = path.dirname(url.fileURLToPath(import.meta.url));
const fontsDir = path.join(scriptDir, '../assets/fonts');
await downloadGoogleFont('Barlow', path.join(fontsDir, 'Barlow'));
};
await main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment