diff --git a/packages/utils/src/minification-data.ts b/packages/utils/src/minification-data.ts index bb35812..8457c1c 100644 --- a/packages/utils/src/minification-data.ts +++ b/packages/utils/src/minification-data.ts @@ -1,63 +1,150 @@ import minificationData from "../../../data/minification-benchmarks-data.json"; -export const popularMinifiers = ["terser", "esbuild", "@swc/core", "uglify-js", "oxc-minify"]; +const MINIFIERS = [ + { id: "terser", name: "Terser", fill: "#a78bfa" }, + { id: "esbuild", name: "ESBuild", fill: "#10b981" }, + { id: "@swc/core", name: "SWC", fill: "#38bdf8" }, + { id: "uglify-js", name: "UglifyJS", fill: "#f87171" }, + { id: "oxc-minify", name: "OXC", fill: "#fbbf24" }, +] as const; -export const libraries = Object.entries(minificationData) - .map(([name, data]: [string, any]) => ({ name, size: data.size })) +type MinifierDefinition = (typeof MINIFIERS)[number]; +type MinifierId = MinifierDefinition["id"]; +type Metric = "time" | "compression"; + +interface BenchmarkResultData { + time?: number; + minzippedBytes?: number; +} + +interface MinifierBenchmark { + result?: { + data?: BenchmarkResultData; + }; +} + +interface LibraryBenchmark { + size: number; + minified?: Record; +} + +type MinificationBenchmarkData = Record; + +interface LibraryDataPoint { + name: string; + value: number; + minzippedBytes: number; + fill: string; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function isBenchmarkResultData(value: unknown): value is BenchmarkResultData { + return ( + isRecord(value) && + (value.time === undefined || typeof value.time === "number") && + (value.minzippedBytes === undefined || typeof value.minzippedBytes === "number") + ); +} + +function isMinifierBenchmark(value: unknown): value is MinifierBenchmark { + if (!isRecord(value)) { + return false; + } + + if (value.result === undefined) { + return true; + } + + return ( + isRecord(value.result) && + (value.result.data === undefined || isBenchmarkResultData(value.result.data)) + ); +} + +function isLibraryBenchmark(value: unknown): value is LibraryBenchmark { + if (!isRecord(value) || typeof value.size !== "number") { + return false; + } + + if (value.minified === undefined) { + return true; + } + + return ( + isRecord(value.minified) && + Object.values(value.minified).every( + (minifierBenchmark) => + minifierBenchmark === undefined || isMinifierBenchmark(minifierBenchmark), + ) + ); +} + +function getBenchmarkData(data: unknown): MinificationBenchmarkData { + if (!isRecord(data)) { + return {}; + } + + return Object.entries(data).reduce((libraries, [name, benchmark]) => { + if (isLibraryBenchmark(benchmark)) { + libraries[name] = benchmark; + } + + return libraries; + }, {}); +} + +const benchmarkData = getBenchmarkData(minificationData); + +export const popularMinifiers: MinifierId[] = MINIFIERS.map(({ id }) => id); + +export const libraries = Object.entries(benchmarkData) + .map(([name, { size }]) => ({ name, size })) .toSorted((a, b) => b.size - a.size) .map((item) => item.name); -export const getLibraryData = (library: string, metric: "time" | "compression") => { - const libraryData = (minificationData as Record)[library]; - const data: any[] = []; - - popularMinifiers.forEach((minifier) => { - const minifierData = libraryData.minified?.[minifier]; - if (minifierData?.result?.data) { - let value: number; - let minzippedBytes = 0; - if (metric === "time") { - value = Math.round(minifierData.result.data.time || 0); - } else { - // compression ratio - const originalSize = libraryData.size; - minzippedBytes = minifierData.result.data.minzippedBytes || 0; - value = Math.round(((originalSize - minzippedBytes) / originalSize) * 100 * 10) / 10; - } - - data.push({ - name: - minifier === "@swc/core" - ? "SWC" - : minifier === "uglify-js" - ? "UglifyJS" - : minifier === "oxc-minify" - ? "OXC" - : minifier === "esbuild" - ? "ESBuild" - : minifier === "terser" - ? "Terser" - : minifier, - value, - minzippedBytes: minzippedBytes || 0, - fill: - minifier === "terser" - ? "#a78bfa" - : minifier === "esbuild" - ? "#10b981" - : minifier === "@swc/core" - ? "#38bdf8" - : minifier === "uglify-js" - ? "#f87171" - : minifier === "oxc-minify" - ? "#fbbf24" - : "#9ca3af", - }); - } - }); +function getMetricValue( + metric: Metric, + librarySize: number, + resultData: BenchmarkResultData, +): number { + if (metric === "time") { + return Math.round(resultData.time || 0); + } - // Sort data: time from smallest to largest (fastest to slowest), compression from largest to smallest (best to worst) - return data.toSorted((a, b) => - metric === "time" ? a.value - b.value : a.minzippedBytes - b.minzippedBytes, - ); -}; + const minzippedBytes = resultData.minzippedBytes || 0; + return Math.round(((librarySize - minzippedBytes) / librarySize) * 100 * 10) / 10; +} + +function toLibraryDataPoint( + minifier: MinifierDefinition, + libraryData: LibraryBenchmark, + metric: Metric, +): LibraryDataPoint | null { + const resultData = libraryData.minified?.[minifier.id]?.result?.data; + if (!resultData) { + return null; + } + + return { + name: minifier.name, + value: getMetricValue(metric, libraryData.size, resultData), + minzippedBytes: metric === "compression" ? resultData.minzippedBytes || 0 : 0, + fill: minifier.fill, + }; +} + +export function getLibraryData(library: string, metric: Metric): LibraryDataPoint[] { + const libraryData = benchmarkData[library]; + if (!libraryData) { + return []; + } + + return MINIFIERS.map((minifier) => toLibraryDataPoint(minifier, libraryData, metric)) + .filter((item): item is LibraryDataPoint => item !== null) + .toSorted((a, b) => + metric === "time" ? a.value - b.value : a.minzippedBytes - b.minzippedBytes, + ); +}