|
| 1 | +import fsp from "node:fs/promises"; |
| 2 | +import { hash } from "ohash"; |
| 3 | +import { extname, join } from "pathe"; |
| 4 | +import { filename } from "pathe/utils"; |
| 5 | +import { hasProtocol, joinRelativeURL, joinURL } from "ufo"; |
| 6 | +import type { FontFaceData } from "unifont"; |
| 7 | +import { storage } from "../cache"; |
| 8 | +import type { Options, RawFontFaceData } from "../types"; |
| 9 | +import { formatToExtension, parseFont } from "./render"; |
| 10 | + |
| 11 | +const renderedFontURLs = new Map<string, string>(); |
| 12 | + |
| 13 | +export async function setupPublicAssetStrategy(options: Options) { |
| 14 | + const { module } = options; |
| 15 | + |
| 16 | + const assetsBaseURL = module.assets.prefix || "/fonts"; |
| 17 | + |
| 18 | + function normalizeFontData( |
| 19 | + faces: RawFontFaceData | FontFaceData[] |
| 20 | + ): FontFaceData[] { |
| 21 | + const data: FontFaceData[] = []; |
| 22 | + for (const face of Array.isArray(faces) ? faces : [faces]) { |
| 23 | + data.push({ |
| 24 | + ...face, |
| 25 | + unicodeRange: |
| 26 | + face.unicodeRange === undefined || Array.isArray(face.unicodeRange) |
| 27 | + ? face.unicodeRange |
| 28 | + : [face.unicodeRange], |
| 29 | + src: (Array.isArray(face.src) ? face.src : [face.src]).map((src) => { |
| 30 | + const source = typeof src === "string" ? parseFont(src) : src; |
| 31 | + if ( |
| 32 | + "url" in source && |
| 33 | + hasProtocol(source.url, { acceptRelative: true }) |
| 34 | + ) { |
| 35 | + source.url = source.url.replace(/^\/\//, "https://"); |
| 36 | + const file = [ |
| 37 | + // TODO: investigate why negative ignore pattern below is being ignored |
| 38 | + filename(source.url.replace(/\?.*/, "")).replace(/^-+/, ""), |
| 39 | + hash(source) + |
| 40 | + (extname(source.url) || formatToExtension(source.format) || ""), |
| 41 | + ] |
| 42 | + .filter(Boolean) |
| 43 | + .join("-"); |
| 44 | + |
| 45 | + renderedFontURLs.set(file, source.url); |
| 46 | + source.originalURL = source.url; |
| 47 | + |
| 48 | + source.url = options.fontless.dev |
| 49 | + ? joinRelativeURL(assetsBaseURL, file) |
| 50 | + : joinURL(assetsBaseURL, file); |
| 51 | + } |
| 52 | + |
| 53 | + return source; |
| 54 | + }), |
| 55 | + }); |
| 56 | + } |
| 57 | + return data; |
| 58 | + } |
| 59 | + |
| 60 | + const cacheDir = join(options.fontless.buildDir, "cache", "fonts"); |
| 61 | + |
| 62 | + if (!options.fontless.dev) { |
| 63 | + await fsp.mkdir(cacheDir, { recursive: true }); |
| 64 | + } |
| 65 | + |
| 66 | + const rollupBefore = async () => { |
| 67 | + if (options.fontless.dev) { |
| 68 | + await fsp.mkdir(join(options.fontless.baseURL, assetsBaseURL), { |
| 69 | + recursive: true, |
| 70 | + }); |
| 71 | + } |
| 72 | + for (const [filename, url] of renderedFontURLs) { |
| 73 | + const key = "data:fonts:" + filename; |
| 74 | + // Use storage to cache the font data between builds |
| 75 | + let res = await storage.getItemRaw(key); |
| 76 | + if (!res) { |
| 77 | + res = await fetch(url) |
| 78 | + .then((r) => r.arrayBuffer()) |
| 79 | + .then((r) => Buffer.from(r)); |
| 80 | + |
| 81 | + await storage.setItemRaw(key, res); |
| 82 | + } |
| 83 | + |
| 84 | + await fsp.writeFile(join(cacheDir, filename), res); |
| 85 | + |
| 86 | + if (options.fontless.dev) { |
| 87 | + await fsp.writeFile( |
| 88 | + joinRelativeURL(options.fontless.baseURL, assetsBaseURL, filename), |
| 89 | + res |
| 90 | + ); |
| 91 | + } |
| 92 | + } |
| 93 | + }; |
| 94 | + |
| 95 | + options.hooks["rollup:before"] = rollupBefore; |
| 96 | + |
| 97 | + return { |
| 98 | + normalizeFontData, |
| 99 | + }; |
| 100 | +} |
0 commit comments