Skip to content

Instantly share code, notes, and snippets.

@jonasgeiler
Created December 11, 2021 23:17
Show Gist options
  • Save jonasgeiler/71380bdb0d7b1eacbb584c6c80ecb6d1 to your computer and use it in GitHub Desktop.
Save jonasgeiler/71380bdb0d7b1eacbb584c6c80ecb6d1 to your computer and use it in GitHub Desktop.
A little script which copies fonts installed with fontsource from the installation path to a local folder
const path = require('path');
const fs = require('fs/promises');
// ---------------------------------------- CONFIG ---------------------------------------- //
/**
* Add the fonts to copy here.
* Make sure you've installed the Fontsource package for the font!
* Uses the same font family format as the Google Font API.
* Examples:
* - 'Roboto:300,400,500,700'
* - 'Source Sans Pro:300,300-italic,400,500'
* @type {string[]}
*/
const fonts = [
'Roboto:300,400,500,700',
];
/**
* Path where the CSS files get copied to.
* Relative to this file.
* @type {string}
*/
const cssOutputPath = '../app/styles/fonts';
/**
* Whether to bundle all CSS files together into one.
* @type {boolean}
*/
const bundleCss = true;
/**
* Base URL used in the CSS files to import the font files.
* For example if your fonts are accessible through 'https://example.com/fonts/...', set this to '/fonts'.
* @type {string}
*/
const fontsBaseURL = '/fonts';
/**
* Path where the font files get copied to.
* Relative to this file.
* @type {string}
*/
const fontsOutputPath = '../public/fonts';
// ---------------------------------------------------------------------------------------- //
(async () => {
for (let font of fonts) {
const [ fontName, sizes ] = parseFontFamily(font);
const moduleName = `@fontsource/${fontName}`;
let cssStrings = [];
let allCssUrls = [];
for (let size of sizes) {
const inputFile = resolveModule(moduleName, `${size}.css`);
const inputCss = await readFile(inputFile);
const cssUrls = getCssUrls(inputCss);
allCssUrls = [ ...allCssUrls, ...cssUrls ];
const baseUrl = joinPath(fontsBaseURL, fontName);
const outputCss = replaceCssBaseURLs(inputCss, baseUrl);
if (bundleCss) {
cssStrings.push(outputCss);
console.log(`Added '${inputFile}' to CSS bundle.`);
} else {
const outputFile = path.resolve(path.join(__dirname, cssOutputPath, fontName, `${size}.css`));
await writeFile(outputFile, outputCss);
console.log(`Transformed '${inputFile}' and wrote it to '${outputFile}'.`);
}
}
if (bundleCss) {
const outputCss = cssStrings.join('\n');
const outputFile = path.resolve(path.join(__dirname, cssOutputPath, `${fontName}.css`));
await writeFile(outputFile, outputCss);
console.log(`Wrote CSS bundle to '${outputFile}'.`);
}
const fontFiles = allCssUrls.filter((v, i, a) => a.indexOf(v) === i); // Get an array of font files without duplicates
for (let fontFile of fontFiles) {
const srcPath = resolveModule(moduleName, fontFile);
const destPath = path.resolve(path.join(__dirname, fontsOutputPath, fontName, path.basename(fontFile)));
await copyFile(srcPath, destPath);
console.log(`Copied '${srcPath}' to '${destPath}'.`);
}
}
})();
const CSS_URL_REGEX = /(?<start>url\(["']?)(?<url>.*?)(?<end>["']?\))/gi;
/**
* Parses a font family formatted like in the Google Font API.
* Examples:
* - 'Roboto:300,400,500,700'
* - 'Source Sans Pro:300,300-italic,400,500'
* @param {string} fontFamily - The string to parse.
* @returns {[ string, number[] ]} - A tuple with the sanitized font name and an array of font sizes
*/
function parseFontFamily(fontFamily) {
let [ name, sizes ] = fontFamily.split(':').map(s => s.trim()); // Separate font family and sizes
name = name.toLowerCase().replace(/[^A-Za-z0-9]+/, '-'); // Sanitize font name
sizes = sizes.split(',').map(s => +s.trim()); // Convert sizes string to array of numbers
return [ name, sizes ];
}
/**
* Resolves the path to a Node.js module.
* @param {string} moduleName - Name of the Node.js module.
* @param {string} additionalPath - Additional path to a specific file or folder in the Node.js module.
* @returns {string} - The path to a file or folder (the entry file, if no additional path specified).
*/
function resolveModule(moduleName, ...additionalPath) {
const modulePath = path.join(moduleName, ...additionalPath);
try {
return require.resolve(modulePath);
} catch (e) {
console.error(`Could not resolve '${modulePath}'. Did you install '${moduleName}'?`);
console.warn(e);
process.exit(1);
}
}
/**
* Read a file.
* @param {string} file - Path to the file.
* @returns {Promise<string>}
*/
async function readFile(file) {
try {
return await fs.readFile(file, { encoding: 'utf8' });
} catch (e) {
console.error(`Could not read file: '${file}'`);
console.warn(e);
process.exit(1);
}
}
/**
* Ensures a directory exists, including it's parent directories.
* @param {string} dir - The path to the directory.
* @returns {Promise<void>}
*/
async function ensureDir(dir) {
try {
await fs.mkdir(dir, { recursive: true });
} catch (e) {
console.error(`Could not ensure folder exists: '${dir}'`);
console.warn(e);
process.exit(1);
}
}
/**
* Write a file.
* @param {string} file - Path to the file.
* @param {string} data - Data to write to the file.
* @returns {Promise<void>}
*/
async function writeFile(file, data) {
try {
await ensureDir(path.dirname(file));
await fs.writeFile(file, data);
} catch (e) {
console.error(`Could not write file: '${file}'`);
console.warn(e);
process.exit(1);
}
}
/**
* Copy a file.
* @param {string} src - Source path.
* @param {string} dest - Destination path.
* @returns {Promise<void>}
*/
async function copyFile(src, dest) {
try {
await ensureDir(path.dirname(dest));
await fs.copyFile(src, dest);
} catch (e) {
console.error(`Could not copy file: '${src}', to: '${dest}'`);
console.warn(e);
process.exit(1);
}
}
/**
* Joins path segments together using '/', taking into account already existing '/'.
* @param {string} segments - The path segments.
*/
function joinPath(...segments) {
return segments
.map((segment, index) => {
if (segment.startsWith('/') && index !== 0) segment = segment.substr(1);
if (segment.endsWith('/')) segment = segment.slice(0, -1);
return segment;
})
.join('/');
}
/**
* Returns an array of all `url(...)` statement URLs.
* @param css - The CSS to parse.
* @returns {string[]}
*/
function getCssUrls(css) {
const matches = css.matchAll(CSS_URL_REGEX);
let urls = [];
for (let match of matches) {
urls.push(match.groups.url);
}
return urls;
}
/**
* Replaces the base URL of all `url(...)` statements with the specified one.
* So, for example, `url('./files/roboto-normal.woff2')` will be replaced by `url('/fonts/roboto-normal.woff2')` when `baseUrl` is set to '/fonts'.
* @param {string} css - The CSS to transpile
* @param {string} baseUrl - The base URL to use.
* @returns - The resulting CSS.
*/
function replaceCssBaseURLs(css, baseUrl) {
return css.replace(CSS_URL_REGEX, (_, start, url, end) => {
const newUrl = `${baseUrl}/${path.basename(url)}`;
return start + newUrl + end;
});
}
@mnowotnik
Copy link

Very handy, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment