"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { FontExtractor: () => FontExtractor, default: () => FontExtractor }); module.exports = __toCommonJS(src_exports); // src/extractor.ts var import_vite3 = require("vite"); var import_node_path3 = require("path"); var import_fontext = require("fontext"); // src/cache.ts var import_node_fs = require("fs"); // src/utils.ts var import_vite = require("vite"); var import_node_path = require("path"); var import_node_crypto = require("crypto"); // src/constants.ts var PLUGIN_NAME = "vite-font-extractor-plugin"; var GOOGLE_FONT_URL_RE = /["'](.+fonts.googleapis.com.+)["']/g; var FONT_URL_REGEX = /url\(['"]?(.*?)['"]?\)/g; var FONT_FAMILY_RE = /font-family:\s*(.*?);/; var SUPPORT_START_FONT_REGEX = /otf|ttf|woff|woff2|ttc|dfont/; var FONT_FACE_BLOCK_REGEX = /@font-face\s*{([\s\S]*?)}/g; var SUPPORTED_RESULTS_FORMATS = ["woff2", "woff", "svg", "eot", "ttf"]; var PROCESS_EXTENSION = ".fef"; var GLYPH_REGEX = /content\s*:[^};]*?('|")(.*?)\s*('|"|;)/g; var UNICODE_REGEX = /\\(\w{4})/; var SYMBOL_REGEX = /"(.)"/; // src/utils.ts var mergePath = (...paths) => (0, import_vite.normalizePath)((0, import_node_path.join)(...paths.filter(Boolean))); var getHash = (text, length = 8) => (0, import_node_crypto.createHash)("sha256").update(text).digest("hex").substring(0, length); var getExtension = (filename) => (0, import_node_path.extname)(filename).slice(1); var getFontExtension = (fontFileName) => getExtension(fontFileName); function exists(value) { return value !== null && value !== void 0; } function intersection(array1, array2) { return array1.filter((item) => array2.includes(item)); } function hasDifferent(array1, array2) { if (array1.length !== array2.length) { return true; } const [biggest, lowest] = array1.length > array2.length ? [array1, array2] : [array2, array1]; return biggest.some((item) => !lowest.includes(item)); } function createResolvers(config) { let fontResolve; return { get common() { return fontResolve ?? (fontResolve = config.createResolver({ extensions: [], tryIndex: false, preferRelative: false })); }, get font() { return fontResolve ?? (fontResolve = config.createResolver({ extensions: SUPPORTED_RESULTS_FORMATS, tryIndex: false, preferRelative: false })); } }; } var extractFontFaces = (code) => { const faces = []; let match = null; FONT_FACE_BLOCK_REGEX.lastIndex = 0; while (match = FONT_FACE_BLOCK_REGEX.exec(code)) { const face = match[0]; if (face) { faces.push(face); } } return faces; }; var extractFonts = (fontFaceString) => { const fonts = []; let match = null; FONT_URL_REGEX.lastIndex = 0; while (match = FONT_URL_REGEX.exec(fontFaceString)) { const url = match[1]; if (url) { fonts.push(url); } } return fonts; }; var extractGoogleFontsUrls = (code) => { const urls = []; let match = null; GOOGLE_FONT_URL_RE.lastIndex = 0; while (match = GOOGLE_FONT_URL_RE.exec(code)) { const url = match[1]; if (url) { urls.push(url); } } return urls; }; var extractFontName = (fontFaceString) => { const fontName = FONT_FAMILY_RE.exec(fontFaceString)?.[1]; return fontName?.replace(/["']/g, "") ?? ""; }; var findUnicodeGlyphs = (code) => { const matches = code.match(GLYPH_REGEX) || []; return matches.map((match) => { const [, unicodeMatch] = match.match(UNICODE_REGEX) || []; if (unicodeMatch) { return String.fromCharCode(parseInt(unicodeMatch, 16)); } const [, symbolMatch] = match.match(SYMBOL_REGEX) || []; if (symbolMatch) { return symbolMatch; } return ""; }).filter(Boolean); }; var escapeString = (value) => value.replaceAll(" ", "\\ "); // src/cache.ts var import_fast_glob = __toESM(require("fast-glob"), 1); var Cache = class { path; constructor(to) { this.path = mergePath(to, ".font-extractor-cache"); this.createDir(); } get exist() { return (0, import_node_fs.existsSync)(this.path); } check(key) { return (0, import_node_fs.existsSync)(this.getPathTo(key)); } get(key) { return (0, import_node_fs.readFileSync)(this.getPathTo(key)); } set(key, data) { this.createDir(); (0, import_node_fs.writeFileSync)(this.getPathTo(key), data); } createDir() { if (this.exist) { return; } (0, import_node_fs.mkdirSync)(this.path, { recursive: true }); } clearCache(pattern) { const remove = (target, recursive = false) => { (0, import_node_fs.rmSync)(target, { recursive }); this.createDir(); }; if (pattern) { import_fast_glob.default.sync(pattern + "/**", { absolute: true, onlyFiles: true, cwd: this.path }).forEach((target) => { remove(target); }); } else { remove(this.path, true); } } getPathTo(...to) { return mergePath(this.path, ...to); } }; // src/extractor.ts var import_node_fs2 = require("fs"); // src/styler.ts var import_picocolors = __toESM(require("picocolors"), 1); var import_node_path2 = require("path"); var DEFAULT = (message) => message; var aliases = { warn: import_picocolors.default.yellow, tag: import_picocolors.default.cyan, error: import_picocolors.default.red, path: (message) => [import_picocolors.default.dim((0, import_node_path2.dirname)(message) + import_node_path2.sep), import_picocolors.default.green((0, import_node_path2.basename)(message))].join("") }; var styler_default = new Proxy({}, { get(_, key) { if (key in aliases) { return aliases[key]; } if (key in import_picocolors.default) { return (message) => import_picocolors.default[key](message); } return DEFAULT; } }); // src/internal-loger.ts var import_vite2 = require("vite"); var createInternalLogger = (logLevel, customLogger) => { const prefix = `[${PLUGIN_NAME}]`; const logger = (0, import_vite2.createLogger)(logLevel, { prefix, customLogger, allowClearScreen: true }); let needFix = false; const log = (level, message, options) => { if (needFix) { logger.info(""); needFix = false; } const tag = options?.timestamp ? "" : styler_default.tag(prefix) + " "; logger[level](`${tag}${styler_default[level](message)}`, options); }; const error = (message, options) => { log("error", message, options); }; const warn = (message, options) => { log("warn", message, options); }; const info = (message, options) => { log("info", message, options); }; return { error, warn, info, fix: () => { needFix = true; } }; }; // src/extractor.ts var import_lodash = __toESM(require("lodash.groupby"), 1); var import_lodash2 = __toESM(require("lodash.camelcase"), 1); function FontExtractor(pluginOption = { type: "auto" }) { const mode = pluginOption.type ?? "manual"; let cache; let importResolvers; let logger; let isServe = false; const fontServeProxy = /* @__PURE__ */ new Map(); const glyphsFindMap = /* @__PURE__ */ new Map(); const autoTarget = new Proxy( { fontName: "ERROR: Illegal access. Font name must be provided from another place instead it", raws: [], withWhitespace: true, ligatures: [] }, { get(target, key) { if (key === "fontName") { throw Error(target[key]); } if (key === "raws") { return Array.from(glyphsFindMap.values()).flat(); } return target[key]; } } ); const autoProxyOption = new Proxy({ sid: "[calculating...]", target: autoTarget, auto: true }, { get(target, key) { if (key === "sid") { return JSON.stringify(autoTarget.raws); } return target[key]; } }); const targets = pluginOption.targets ? Array.isArray(pluginOption.targets) ? pluginOption.targets : [pluginOption.targets] : []; const casualOptionsMap = new Map( targets.map((target) => [target.fontName, { sid: JSON.stringify(target), target, auto: false }]) ); const optionsMap = { get: (key) => { const option = casualOptionsMap.get(key); return mode === "auto" ? option ?? autoProxyOption : option; }, has: (key) => mode === "auto" || casualOptionsMap.has(key) }; const progress = /* @__PURE__ */ new Map(); const transformMap = /* @__PURE__ */ new Map(); const changeResource = function(code, transform) { const sid = getHash(transform.sid); const assetUrlRE = /__VITE_ASSET__([\w$]+)__(?:\$_(.*?)__)?/g; const oldReferenceId = assetUrlRE.exec(transform.alias)[1]; const referenceId = this.emitFile({ type: "asset", name: transform.name + PROCESS_EXTENSION, source: Buffer.from(sid + oldReferenceId) }); transformMap.set(oldReferenceId, referenceId); return code.replace(transform.alias, transform.alias.replace(oldReferenceId, referenceId)); }; const getSourceByUrl = async (url, importer) => { const entrypointFilePath = await importResolvers.font(url, importer); if (!entrypointFilePath) { logger.warn(`Can not resolve entrypoint font by url: ${styler_default.path(url)}`); return null; } return (0, import_node_fs2.readFileSync)(entrypointFilePath); }; const processMinify = async (fontName, fonts, options) => { const unsupportedFont = fonts.find((font) => !SUPPORTED_RESULTS_FORMATS.includes(font.extension)); if (unsupportedFont) { logger.error(`Font face has unsupported extension - ${unsupportedFont.extension ?? "undefined"}`); return null; } const entryPoint = fonts.find((font) => SUPPORT_START_FONT_REGEX.test(font.extension)); if (!entryPoint) { logger.error("No find supported fonts file extensions for extracting process"); return null; } const sid = options.sid; const cacheKey = (0, import_lodash2.default)(fontName) + "-" + getHash(sid + entryPoint.url); const needExtracting = fonts.some((font) => !cache?.check(cacheKey + `.${font.extension}`)); const minifiedBuffers = { meta: [] }; if (needExtracting) { if (cache) { logger.info(`Clear cache for ${fontName} because some files have a different content`); cache.clearCache(fontName); } const source = entryPoint.source ?? await getSourceByUrl(entryPoint.url, entryPoint.importer); if (!source) { logger.error(`No found source for ${fontName}:${styler_default.path(entryPoint.url)}`); return null; } const minifyResult = await (0, import_fontext.extract)( Buffer.from(source), { fontName, formats: fonts.map((font) => font.extension), raws: options.target.raws, ligatures: options.target.ligatures, withWhitespace: options.target.withWhitespace } ); Object.assign(minifiedBuffers, minifyResult); if (cache) { fonts.forEach((font) => { const minifiedBuffer = minifyResult[font.extension]; if (minifiedBuffer) { logger.info(`Save a minified buffer for ${fontName} to cache`); cache.set(cacheKey + `.${font.extension}`, minifiedBuffer); } }); } } else { logger.info(`Get minified fonts from cache for ${fontName}`); const cacheResult = Object.fromEntries( fonts.map((font) => [font.extension, cache.get(cacheKey + `.${font.extension}`)]) ); Object.assign(minifiedBuffers, cacheResult); } return minifiedBuffers; }; const checkFontProcessing = (name, id) => { const duplicateId = progress.get(name); if (duplicateId && !isServe) { const placeInfo = `Font placed in "${styler_default.path(id)}" and "${styler_default.path(duplicateId)}"`; const errorMessage = `Plugin not support a multiply files with same font name [${name}]. ${placeInfo}`; logger.error(errorMessage); throw new Error(errorMessage); } else { progress.set(name, id); } }; const processServeFontMinify = (id, url, fontName) => { let result = null; let prevOptions = optionsMap.get(fontName); return async () => { const currentOptions = optionsMap.get(fontName); if (currentOptions && (!result || currentOptions.sid !== prevOptions?.sid)) { prevOptions = currentOptions; const extension = getFontExtension(url); const minifiedBuffers = await processMinify( fontName, [{ url, importer: id, extension }], currentOptions ); const content = minifiedBuffers?.[extension]; if (content) { result = { content, extension, id }; } else { result = null; } } return result; }; }; const processServeAutoFontMinify = (id, url, fontName) => { let previousRaws = autoProxyOption.target.raws ?? []; let result; return async () => { const currentRaws = autoProxyOption.target.raws ?? []; if (!result || hasDifferent(previousRaws, currentRaws)) { previousRaws = currentRaws; const extension = getFontExtension(url); const minifiedBuffers = await processMinify( fontName, [{ url, importer: id, extension }], autoProxyOption ); const content = minifiedBuffers?.[extension]; if (content) { result = { content, extension, id }; } else { result = null; } } return result; }; }; const loadedAutoFontMap = /* @__PURE__ */ new Map(); const processFont = async function(code, id, font) { checkFontProcessing(font.name, id); if (isServe) { font.aliases.forEach((url) => { if (fontServeProxy.has(url)) { return; } const process = font.options.auto ? processServeAutoFontMinify(id, url, font.name) : processServeFontMinify(id, url, font.name); fontServeProxy.set( url, process ); if (font.options.auto) { loadedAutoFontMap.set(url, false); } }); } else { if (mode === "auto") { const message = `"auto" mod detected. "${font.name}" font is stubbed and result file hash will be recalculated randomly that may potential problem with external cache systems. If this font is not target please add it to ignore`; logger.warn(message); } font.aliases.forEach((alias) => { code = changeResource.call( this, code, { alias, name: font.name, // TODO: must be reworked sid: mode === "auto" ? Math.random().toString() : font.options.sid } ); }); } return code; }; const processGoogleFontUrl = function(code, id, font) { checkFontProcessing(font.name, id); const oldText = font.url.searchParams.get("text"); if (oldText) { logger.warn(`Font [${font.name}] in ${id} has duplicated logic for minification`); } const text = [oldText, ...font.options.target.ligatures ?? []].filter(exists).join(" "); const originalUrl = font.url.toString(); const fixedUrl = new URL(originalUrl); fixedUrl.searchParams.set("text", text); return code.replace(originalUrl, fixedUrl.toString()); }; return { name: PLUGIN_NAME, configResolved(config) { logger = createInternalLogger(pluginOption.logLevel ?? config.logLevel, config.customLogger); logger.fix(); logger.info(`Plugin starts in "${mode}" mode`); const intersectionIgnoreWithTargets = intersection(pluginOption.ignore ?? [], targets.map((target) => target.fontName)); if (intersectionIgnoreWithTargets.length) { logger.warn(`Ignore option has intersection with targets: ${intersectionIgnoreWithTargets.toString()}`); } importResolvers = createResolvers(config); if (pluginOption.cache) { const cachePath = typeof pluginOption.cache === "string" && pluginOption.cache || "node_modules"; const resolvedPath = (0, import_node_path3.isAbsolute)(cachePath) ? cachePath : mergePath(config.root, cachePath); cache = new Cache(resolvedPath); } }, configureServer(server) { isServe = true; server.middlewares.use((req, res, next) => { const url = req.url; const process = fontServeProxy.get(url); if (!process) { next(); } else { void (async () => { const stub = await process(); if (!stub) { next(); return; } logger.fix(); logger.info(`Stub server response for: ${styler_default.path(url)}`); (0, import_vite3.send)(req, res, stub.content, `font/${stub.extension}`, { cacheControl: "no-cache", headers: server.config.server.headers, // Disable cache for font request etag: "" }); loadedAutoFontMap.set(url, true); })(); } }); }, async transform(code, id) { logger.fix(); const isCssFile = (0, import_vite3.isCSSRequest)(id); const isAutoType = mode === "auto"; const isCssFileWithFontFaces = isCssFile && code.includes("@font-face"); if (isAutoType && isCssFile) { const glyphs = findUnicodeGlyphs(code); glyphsFindMap.set(id, glyphs); } if ((id.endsWith(".html") || isCssFile && code.includes("@import")) && code.includes("fonts.googleapis.com")) { const googleFonts = extractGoogleFontsUrls(code).map((raw) => { const url = new URL(raw); const name = url.searchParams.get("family"); if (pluginOption.ignore?.includes(name)) { return null; } if (!name) { logger.warn(`No specified google font name in ${styler_default.path(id)}`); return null; } if (name.includes("|")) { logger.warn("Google font url includes multiple families. Not supported"); return null; } const options = optionsMap.get(name); if (!options) { logger.warn(`Font "${name}" has no minify options`); return null; } return { name, options, url }; }).filter(exists); for (const font of googleFonts) { try { code = processGoogleFontUrl.call(this, code, id, font); } catch (e) { logger.error(`Process ${font.name} Google font is failed`, { error: e }); } } } if (isCssFileWithFontFaces) { const fonts = extractFontFaces(code).map((face) => { const name = extractFontName(face); if (pluginOption.ignore?.includes(name)) { return null; } const options = optionsMap.get(name); if (!options) { logger.warn(`Font "${name}" has no minify options`); return null; } const aliases2 = extractFonts(face); const urlSources = aliases2.filter((alias) => alias.startsWith("http")); if (urlSources.length) { logger.warn(`Font "${name}" has external url sources: ${urlSources.toString()}`); return null; } return { name, face, aliases: aliases2, options }; }).filter(exists); for (const font of fonts) { try { code = await processFont.call(this, code, id, font); } catch (e) { logger.error(`Process ${font.name} local font is failed`, { error: e }); } } } return code; }, async generateBundle(_, bundle) { if (!transformMap.size) { return; } logger.fix(); try { const findAssetByReferenceId = (referenceId) => Object.values(bundle).find( (asset) => asset.fileName.includes(this.getFileName(referenceId)) ); const resources = Array.from(transformMap.entries()).map(([oldReferenceId, newReferenceId]) => { return [ findAssetByReferenceId(oldReferenceId), findAssetByReferenceId(newReferenceId) ]; }); const unminifiedFonts = (0, import_lodash.default)( resources.filter(([_2, newFont]) => newFont.fileName.endsWith(PROCESS_EXTENSION)), ([_2, newFont]) => newFont.name.replace(PROCESS_EXTENSION, "") ); const stringAssets = Object.values(bundle).filter((asset) => asset.type === "asset" && typeof asset.source === "string"); await Promise.all(Object.entries(unminifiedFonts).map(async ([fontName, transforms]) => { const minifiedBuffer = await processMinify( fontName, transforms.map(([originalFont, newFont]) => ({ extension: getFontExtension(originalFont.fileName), source: Buffer.from(originalFont.source), url: "" })), optionsMap.get(fontName) ); transforms.forEach(([originalFont, newFont]) => { const extension = getFontExtension(originalFont.fileName); const fixedName = originalFont.name ? (0, import_node_path3.basename)(originalFont.name, `.${extension}`) : (0, import_lodash2.default)(fontName); const temporalNewFontFilename = newFont.fileName; const fixedBasename = ((0, import_node_path3.basename)(newFont.fileName, PROCESS_EXTENSION) + `.${extension}`).replace(fontName, fixedName); newFont.name = fixedName + `.${extension}`; newFont.fileName = newFont.fileName.replace((0, import_node_path3.basename)(temporalNewFontFilename), fixedBasename); const temporalNewFontBasename = escapeString(temporalNewFontFilename); stringAssets.forEach((asset) => { if (asset.source.includes(temporalNewFontBasename)) { logger.info(`Change name from "${styler_default.green(temporalNewFontBasename)}" to "${styler_default.green(newFont.fileName)}" in ${styler_default.path(asset.fileName)}`); asset.source = asset.source.replace(temporalNewFontBasename, newFont.fileName); } }); newFont.source = minifiedBuffer?.[extension] ?? Buffer.alloc(0); }); })); resources.forEach(([originalFont, newFont]) => { const originalBuffer = Buffer.from(originalFont.source); const newLength = newFont.source.length; const originalLength = originalBuffer.length; const resultLessThanOriginal = newLength > 0 && newLength < originalLength; if (!resultLessThanOriginal) { const comparePreview = styler_default.red(`[${newLength} < ${originalLength}]`); logger.warn(`New font no less than original ${comparePreview}. Revert content to original font`); newFont.source = originalBuffer; } logger.info(`Delete old asset from: ${styler_default.path(originalFont.fileName)}`); delete bundle[originalFont.fileName]; }); } catch (error) { logger.error("Clean up generated bundle is filed", { error }); } } }; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { FontExtractor });