vite-plugin-font
Version:
An automatic Web Font optimization plugin that supports many platforms such as Vite, Next, Nuxt, and more.
121 lines (115 loc) • 4.49 kB
text/typescript
import { proto, fontSplit, decodeReporter } from 'cn-font-split';
import path from 'path';
import fs from 'fs-extra';
import url from 'url';
import crypto from 'crypto';
import { createChineseCrossPlatformFallbackCss } from 'cn-font-metrics';
export function getFileName(id: string) {
return path.basename(id).replace(/\./g, '_');
}
function chunk(arr?: number[], size = 500) {
if (arr) {
let result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
} else {
return;
}
}
export type BundlePluginConfig = Omit<Partial<proto.InputTemplate>, 'input' | 'subsets'> & {
cacheDir?: string;
server?: boolean;
subsetChunkSize?: number
};
export class BundlePlugin {
public subsets: number[][] | undefined = undefined;
constructor(
public config: BundlePluginConfig,
public key = 'default',
) {}
/** 获取正确的缓存文件夹的位置 */
getCachedPath(p: string) {
return path.resolve(this.config.cacheDir!, getFileName(p));
}
/** 创建 CSS 封装层源代码 */
async createSourceCode(p: string) {
const resolvedPath = this.getCachedPath(p);
const reporter = decodeReporter(
await fs.promises.readFile(resolvedPath + '/reporter.bin'),
);
const metrics = await fs.readJSON(resolvedPath + '/metrics.json');
const details = { ...reporter.toObject(), ...metrics };
const code = Object.entries(details)
.map(([k, v]) => {
return `export const ${k} = ${JSON.stringify(v)};`;
})
.join('\n');
const resultCSS = decodeURI(url.pathToFileURL(resolvedPath).pathname);
const key = (Math.random() * 100000).toFixed(0);
return (
`import '${resultCSS}/metrics.css?t=${key}';\n` +
`import '${resultCSS}/result.css?t=${key}';\n` +
code
);
}
/** 检查整个系统的缓存 */
async checkCache(resolvedPath: string) {
const isFullCached = await Promise.all([
fs.exists(resolvedPath),
fs.exists(path.resolve(resolvedPath, 'result.css')),
fs.exists(path.resolve(resolvedPath, 'metrics.css')),
fs.exists(path.resolve(resolvedPath, 'metrics.json')),
fs.exists(path.resolve(resolvedPath, 'reporter.bin')),
]);
return isFullCached.every((i) => i);
}
/** 重新进行预构建字体 */
async prebuild(filePath: string, mode: 'full' | 'subsets' = 'full') {
const resolvedPath = this.getCachedPath(filePath);
const isCached = await this.checkCache(resolvedPath);
if (!isCached && this.config.server !== false) {
console.log(
'vite-plugin-font | font pre-building | ' + resolvedPath,
);
const FontPath = filePath.split('?')[0];
const onlySubset = mode !== 'full';
await fontSplit({
...this.config,
input: FontPath,
outDir: resolvedPath,
reporter: true,
languageAreas: !onlySubset as false,
autoSubset: true,
fontFeature: true,
reduceMins: !onlySubset,
subsetRemainChars: !onlySubset,
subsets: onlySubset ? chunk(this.subsets?.flat(), this.config.subsetChunkSize) : undefined,
silent: true,
}).catch((e) => {
console.error(e);
});
await this.createCSSFontFallback(FontPath, resolvedPath);
} else {
console.log('vite-plugin-font | using cache | ' + resolvedPath);
}
}
/** 写入 CSS 字体的 fallback 选项,减少布局抖动 */
async createCSSFontFallback(FontPath: string, resolvedPath: string) {
const hash = crypto.createHash('md5').update(FontPath).digest('hex');
const { fontFamilyString, css } =
await createChineseCrossPlatformFallbackCss(
FontPath,
` ${this.key} ${hash.slice(0, 6)}`,
);
await fs.promises.writeFile(
path.resolve(resolvedPath, 'metrics.css'),
css,
);
await fs.promises.writeFile(
path.resolve(resolvedPath, 'metrics.json'),
JSON.stringify({ fontFamilyFallback: fontFamilyString }),
);
}
}