-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
238 additions
and
59 deletions.
There are no files selected for viewing
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
Object.entries(outputBundle).forEach(([filename, bundle]) => { | ||
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.`); | ||
} | ||
}, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |