vite-plugin-font
Version:
An automatic Web Font optimization plugin that supports many platforms such as Vite, Next, Nuxt, and more.
128 lines (124 loc) • 4.5 kB
text/typescript
import { BundlePlugin, BundlePluginConfig, getFileName } from './index.js';
import { glob } from 'glob';
import fs from 'fs-extra';
import crypto from 'node:crypto';
import path from 'path';
import { minimatch } from 'minimatch';
export interface SubsetBundlePluginConfig extends BundlePluginConfig {
scanFiles?: string | string[] | Record<string, string | string[]>;
ignore?: string | string[];
whiteList?: string | string[];
emptyCacheDir?: boolean;
}
export class SubsetUtils {
static isSubsetLink(p: string) {
const [idString, params] = p.split('?');
const searchParams = new URLSearchParams(params);
const isSubset = searchParams.has('subsets');
return { idString, isSubset, searchParams };
}
static emptyCacheDir(config: BundlePluginConfig) {
if (!config.cacheDir)
throw new Error('vite-plugin-font | cacheDir missed');
const dir = path.resolve(config.cacheDir, '.subsets');
return fs.emptyDir(dir);
}
}
export class SubsetBundlePlugin extends BundlePlugin {
/** 构建引擎的文件 ID,提供给构建引擎判断 */
public buildId?: string;
subsetConfig: SubsetBundlePluginConfig;
constructor(config: SubsetBundlePluginConfig, key?: string) {
super(config, key);
this.subsetConfig = config;
this.subsetCacheDir = config.cacheDir!;
}
usedSubsets = new Set<number>();
subsetCacheDir: string;
getCachedPath(p: string) {
const { isSubset, idString } = this.isSubsetLink(p);
return path.resolve(
isSubset ? this.subsetCacheDir : this.config.cacheDir!,
getFileName(idString),
);
}
isSubsetLink = SubsetUtils.isSubsetLink;
async createSubsetsHash() {
if (!this.subsetConfig.scanFiles) return;
this.getWhiteListSubsets();
await this.updateScanFiles();
const subsetsArr = [...this.usedSubsets].sort((a, b) => a - b);
this.subsets = [subsetsArr];
const hash = crypto
.createHash('md5')
.update(String.fromCodePoint(...subsetsArr))
.digest('hex');
// 修改缓存地址,达到缓存
this.subsetCacheDir = path.resolve(
this.config.cacheDir!,
'.subsets',
hash,
);
return hash;
}
/** 匹配绝对路径是否被获取到 */
isMatchScanArea(p: string) {
const globStr = this.getGlobArea();
if (!globStr) return false;
const resolvePath = (p: string) => path.resolve(process.cwd(), p);
if (globStr instanceof Array) {
return globStr.some((i) => minimatch(p, resolvePath(i)));
}
return minimatch(p, resolvePath(globStr));
}
/** 获取配置中的 scanFiles 字段 */
private getGlobArea() {
if (
typeof this.subsetConfig.scanFiles === 'object' &&
!(this.subsetConfig.scanFiles instanceof Array)
) {
return this.subsetConfig.scanFiles[this.key];
} else {
return this.subsetConfig.scanFiles;
}
}
private async updateScanFiles() {
const globStr = this.getGlobArea();
if (!globStr) return;
const files = await glob(globStr!, {
absolute: true,
nodir: true,
ignore: this.subsetConfig.ignore ?? ['node_modules/**'],
});
return Promise.all(
files.map((i) => {
return new Promise((res, rej) => {
const stream = fs.createReadStream(i, {
encoding: 'utf8',
});
stream.on('data', (i) => {
[...(i as string)].forEach((char) =>
this.usedSubsets.add(char.codePointAt(0)!),
);
});
stream.on('end', () => {
res(null);
});
stream.on('error', (err) => {
rej(err);
});
});
}),
);
}
private getWhiteListSubsets() {
let whiteList = this.subsetConfig.whiteList ?? [];
if (!(whiteList instanceof Array)) {
whiteList = [whiteList];
}
const set = this.usedSubsets;
whiteList.forEach((str) =>
[...str].forEach((char) => set.add(char.codePointAt(0)!)),
);
}
}