Created
December 11, 2021 23:17
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Very handy, thanks!