Skip to content

Commit

Permalink
feat: vite plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
Bernankez committed Mar 11, 2024
1 parent 9837264 commit 366a7f2
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 59 deletions.
Binary file added fixtures/vanilla/public/biantaoti.woff
Binary file not shown.
Binary file modified fixtures/vanilla/src/biantaoti.woff
Binary file not shown.
13 changes: 13 additions & 0 deletions fixtures/vanilla/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ import "./style.css";
import typescriptLogo from "./typescript.svg";
import viteLogo from "/vite.svg";
import { setupCounter } from "./counter.ts";

Check failure on line 4 in fixtures/vanilla/src/main.ts

View workflow job for this annotation

GitHub Actions / typecheck

An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.
import Biantaoti from "./biantaoti.woff";

const fontFace = `@font-face {
font-family: BTT2;
src: url("${Biantaoti}");
}`;

const fontFace2 = `@font-face {
font-family: BTT2;
src: url("./biantaoti.woff");
}`;

document.querySelector<HTMLDivElement>("#app")!.innerHTML = `

Check failure on line 17 in fixtures/vanilla/src/main.ts

View workflow job for this annotation

GitHub Actions / typecheck

Cannot find name 'document'. Do you need to change your target library? Try changing the 'lib' compiler option to include 'dom'.

Check failure on line 17 in fixtures/vanilla/src/main.ts

View workflow job for this annotation

GitHub Actions / typecheck

Cannot find name 'HTMLDivElement'.
<div>
Expand All @@ -13,6 +24,8 @@ document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
</a>
<h1>Vite + TypeScript</h1>
<div style="font-family: BTT; font-size: 2rem;">Font test. 中文测试. 1234567</div>
<div style="font-family: BTT2; font-size: 2rem; ${fontFace}">Font test. 中文测试. 1234567</div>
<div style="font-family: BTT2; font-size: 2rem; ${fontFace2}">Font test. 中文测试. 1234567</div>
<div class="card">
<button id="counter" type="button"></button>
</div>
Expand Down
5 changes: 5 additions & 0 deletions fixtures/vanilla/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,9 @@ button:focus-visible {
@font-face {
font-family: BTT2;
src: url("./biantaoti.woff") format("woff");
}

@font-face {
font-family: BTT3;
src: url("/biantaoti.woff") format("woff");
}
18 changes: 14 additions & 4 deletions fixtures/vanilla/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,23 @@ export default defineConfig(() => {
return {
plugins: [
FontCarrier({
fonts: [{
url: "./src/assets/fonts/biantaoti.woff",
input: "中文",
},
fonts: [
{
path: "./src/assets/fonts/biantaoti.woff",
input: "中文",
},
{
path: "./src/assets/fonts/biantaoti2.woff",
input: "中文",
},
],
}),
Inspect(),
],
resolve: {
alias: {
"@": "./src",
},
},
};
});
12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"bugs": "https://github.com/Bernankez/vite-plugin-font-carrier/issues",
"keywords": [
"vite",
"font",
"optimization",
"extractor",
Expand Down Expand Up @@ -59,9 +60,18 @@
"lint": "eslint .",
"fix": "eslint . --fix"
},
"peerDependencies": {
"vite": "*"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
},
"dependencies": {
"@types/font-carrier": "^0.3.3",
"font-carrier": "^0.3.1"
"font-carrier": "^0.3.1",
"kolorist": "^1.8.0"
},
"devDependencies": {
"@bernankez/eslint-config": "^0.7.4",
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions src/env.d.ts

This file was deleted.

195 changes: 146 additions & 49 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,79 +1,176 @@
import { type PluginOption } from "vite";
import { basename, resolve } from "node:path";
import { type IndexHtmlTransformContext, type LogLevel, type Logger, type PluginOption, type ResolveFn, type ResolvedConfig, createLogger } from "vite";
import type { Font as FCFont } from "font-carrier";
import fontCarrier from "font-carrier";
import { bold, green, yellow } from "kolorist";
import { version } from "../package.json";
import { matchFontFace, matchUrl } from "./match";
import { getFileHash, resolvePath } from "./utils";

export interface FontCarrierOptions {
fonts: Font[];
cwd?: string;
type?: FCFont.FontType;
logLevel?: LogLevel;
clearScreen?: boolean;
}

export interface Font {
url: string;
path: string;
input: string;
type?: FCFont.FontType;
}

export const FontCarrier: (options: FontCarrierOptions) => PluginOption = (options) => {
const { cwd = process.cwd(), fonts } = options;

// const fontPath = resolve(cwd, path);
type OutputBundle = Exclude<IndexHtmlTransformContext["bundle"], undefined>;

// if (!output) {
// output = fontPath;
// }
type OutputAssetType<T> = T extends { type: "asset" } ? T : never;
type OutputAsset = OutputAssetType<OutputBundle[keyof OutputBundle]>;

// const font = fontCarrier.transfer(fontPath);

// console.log(font.getFontface().options);
interface FontInfo {
/** Absolute path */
path: string;
/** File base name */
filename: string;
hash: string;
hashname: string;
input: string;
/** Has compressed */
compressed: boolean;
linkedBundle?: OutputAsset;
/** Output font type */
type: FCFont.FontType;
}

// font.min("中文135");
export const FontCarrier: (options: FontCarrierOptions) => PluginOption = (options) => {
const { cwd = process.cwd(), fonts, type, logLevel, clearScreen } = options;

// const res = font.output({
// path: output.split(".").slice(0, -1).join("."),
// types: ["woff2"],
// });
const DEFAULT_FONT_TYPE: FCFont.FontType = "woff2";
const PUBLIC_DIR = resolve(cwd, "public");
const LOG_PREFIX = "[vite-plugin-font-carrier]";

// console.log(res);
const fontList = fonts.map(font => ({
path: resolve(cwd, font.path),
input: font.input,
type: font.type || type || DEFAULT_FONT_TYPE,
matched: false,
}));

// const fontMap = new Map<string, FontFaceParsedInfo>();
const fontCollection: FontInfo[] = [];

// css file url => Fonts
const fontMap = new Map<string, {
url: string; // includes filename
filename: string;
hashname: string;
}[]>();
let resolvedConfig: ResolvedConfig;
let resolver: ResolveFn;
let logger: Logger;

return {
name: "vite-plugin-font-carrier",
version,
enforce: "pre",
transform(code, id) {
const fontFaces = matchFontFace(code);
if (!fontFaces) {
return;
}
const urls = fontFaces.map(fc => matchUrl(fc)).flat().filter(url => url) as string[];
if (!urls) {
return;
}
console.log(fontMap);
configResolved(config) {
resolvedConfig = config;
resolver = resolvedConfig.createResolver();
logger = logLevel ? createLogger(logLevel, { allowClearScreen: clearScreen }) : config.logger;
},
generateBundle: {
transform: {
order: "pre",
handler(outputOptions, bundle, isWrite) {
// console.log(outputOptions);
console.log(Object.values(bundle).map((v) => {
if (v.type === "asset") {
// console.log(v);
// console.log(v.fileName);
} else {
// console.log(v);
// console.log(v.viteMetadata);
async handler(code, id, options) {
const font = fontList.find(font => font.path === id);
if (font) {
// Font imported by js/ts file
const hash = getFileHash(id);
if (hash) {
fontCollection.push({
path: id,
filename: basename(id),
hash,
hashname: "",
input: font.input,
compressed: false,
type: font.type,
});
return;
}
}
// Get font url from source code
const fontFaces = matchFontFace(code);
if (!fontFaces) {
return;
}
// Each fontFace can have multiple Urls
const urls = fontFaces.map(fc => matchUrl(fc)).flat().filter(url => url) as string[];
if (!urls) {
return;
}
for (const url of urls) {
const path = await resolvePath({
id: url,
importer: id,
publicDir: PUBLIC_DIR,
root: resolvedConfig.root,
resolver,
ssr: options?.ssr,
});
const font = fontCollection.find(font => font.path === path);
if (font) {
return;
}
const fontListItem = fontList.find(font => font.path === path);
if (!fontListItem) {
return;
}
const hash = getFileHash(path);
if (hash) {
const fc: FontInfo = {
path,
filename: basename(path),
hash,
hashname: "",
input: fontListItem.input,
compressed: false,
type: fontListItem.type,
};
fontCollection.push(fc);
fontListItem.matched = true;
}
return v.fileName;
}));
// console.log(Object.values(bundle).map(v => v.fileName));
}
},
},
generateBundle(outputOptions, outputBundle, isWrite) {

Check warning on line 136 in src/index.ts

View workflow job for this annotation

GitHub Actions / lint

'isWrite' is defined but never used. Allowed unused args must match /^_/u
Object.entries(outputBundle).forEach(([filename, bundle]) => {

Check warning on line 137 in src/index.ts

View workflow job for this annotation

GitHub Actions / lint

'filename' is defined but never used. Allowed unused args must match /^_/u
if (bundle.type === "asset") {
// Link font filename and hashname
if (bundle.source instanceof Uint8Array) {
const filterFonts = fontCollection.filter(font => font.filename === bundle.name);
if (filterFonts.length > 0) {
const assetHash = getFileHash(bundle.source);
const asset = filterFonts.find(font => font.hash === assetHash);
if (asset) {
asset.hashname = bundle.fileName;
asset.linkedBundle = bundle;
}
}
}
} else {
bundle.viteMetadata?.importedAssets.forEach((asset) => {
const font = fontCollection.find(font => font.hashname === asset);
if (font) {
if (!font.compressed && font.linkedBundle) {
const buffer = Buffer.from(font.linkedBundle.source);
const fc = fontCarrier.transfer(buffer);
fc.min(font.input);
const outputs = fc.output({
types: [font.type],
}) as unknown as { [K in FCFont.FontType]: Buffer };
font.linkedBundle.source = outputs[font.type];
font.compressed = true;
logger.info(`\n${bold(green(LOG_PREFIX))} ${bold(font.filename)} compressed.`);
}
}
});
}
});
const names = fontList.filter(font => !font.matched).map(font => basename(font.path));
if (names.length) {
logger.warn(`${bold(yellow(LOG_PREFIX))} ${bold(names.join(", "))} mistached.`);
}
},
};
};
42 changes: 42 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { type BinaryLike, createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { isAbsolute, resolve } from "node:path";
import type { ResolveFn } from "vite";

export function getFileHash(path: string | BinaryLike) {
if (typeof path === "string") {
try {
const buffer = readFileSync(path);
const hash = createHash("sha256").update(buffer).digest("hex");
return hash;
} catch (e) {
return undefined;
}
} else {
const hash = createHash("sha256").update(path).digest("hex");
return hash;
}
}

export interface ResolvePathOptions {
id: string;
importer: string;
publicDir: string;
root: string;
resolver: ResolveFn;
ssr?: boolean;
}

export async function resolvePath(options: ResolvePathOptions) {
const { id, importer, publicDir, root, resolver, ssr } = options;
let path = await resolver(id, importer, false, ssr);
if (path) {
if (!isAbsolute(path)) {
// Path alias
path = resolve(root, path);
}
} else {
path = resolve(publicDir, `.${id}`);
}
return path;
}

0 comments on commit 366a7f2

Please sign in to comment.